259 lines
7.1 KiB
Go
259 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"net"
|
|
"runtime/debug"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const nativeOverloadBackoff = 10 * time.Millisecond
|
|
|
|
// xrayRecover prevents a bad client packet, closed HTTP stream, or mux/session
|
|
// race from taking down the whole sshpanel process. A panic should only kill the
|
|
// current native Xray connection/session and must always leave a visible stack
|
|
// trace in /api/xray/logs and journald.
|
|
func xrayRecover(where string) {
|
|
if r := recover(); r != nil {
|
|
xrayLogf("native xray: panic recovered in %s: %v\n%s", where, r, debug.Stack())
|
|
}
|
|
}
|
|
|
|
func xrayGo(where string, fn func()) {
|
|
go func() {
|
|
defer xrayRecover(where)
|
|
fn()
|
|
}()
|
|
}
|
|
|
|
func init() {
|
|
// Direct native-inbound tests and embedders may run an accept loop without
|
|
// the singleton server start method. Production stop() flips this to false.
|
|
nativeTransportAccepting.Store(true)
|
|
}
|
|
|
|
var (
|
|
nativeTransportConnections atomic.Int64
|
|
nativeTransportRejected atomic.Int64
|
|
nativeXHTTPRequests atomic.Int64
|
|
nativeXHTTPRequestsRejected atomic.Int64
|
|
nativeXHTTPSessions atomic.Int64
|
|
nativeXHTTPSessionsRejected atomic.Int64
|
|
nativeClientConnsRejected atomic.Int64
|
|
nativePreAuthRejected atomic.Int64
|
|
|
|
nativeTransportAccepting atomic.Bool
|
|
nativeTransportRegistry = struct {
|
|
sync.Mutex
|
|
conns map[*nativeCountedConn]struct{}
|
|
}{conns: make(map[*nativeCountedConn]struct{})}
|
|
)
|
|
|
|
// acquireNativeCounter reserves one slot without blocking. Blocking the accept
|
|
// loop or an HTTP handler when the process is already at its safety ceiling
|
|
// would retain yet more sockets/goroutines, so overload is rejected promptly.
|
|
func acquireNativeCounter(active *atomic.Int64, limit int) (func(), bool) {
|
|
for {
|
|
current := active.Load()
|
|
if limit > 0 && current >= int64(limit) {
|
|
return nil, false
|
|
}
|
|
if active.CompareAndSwap(current, current+1) {
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
if active.Add(-1) < 0 {
|
|
active.Store(0)
|
|
}
|
|
})
|
|
}, true
|
|
}
|
|
}
|
|
}
|
|
|
|
func shouldLogNativeSample(counter *atomic.Int64) (n int64, ok bool) {
|
|
n = counter.Add(1)
|
|
// Keep attacks visible without allowing logging itself to become a CPU/disk
|
|
// amplifier. The first event and one event per 1024 repetitions are logged.
|
|
return n, n == 1 || n%1024 == 0
|
|
}
|
|
|
|
func logNativeLimitRejection(kind string, rejected *atomic.Int64, limit int) {
|
|
n, ok := shouldLogNativeSample(rejected)
|
|
if ok {
|
|
xrayLogf("native xray: rejected %s at safety limit=%d (rejected=%d)", kind, limit, n)
|
|
}
|
|
}
|
|
|
|
func logNativeClientLimitRejection(email string, limit int) {
|
|
n, ok := shouldLogNativeSample(&nativeClientConnsRejected)
|
|
if ok {
|
|
xrayLogf("native xray: rejected authenticated user %s at max_conns=%d (rejected=%d)", email, limit, n)
|
|
}
|
|
}
|
|
|
|
func logNativePreAuthRejection(format string, args ...interface{}) {
|
|
if _, ok := shouldLogNativeSample(&nativePreAuthRejected); ok {
|
|
xrayLogf(format, args...)
|
|
}
|
|
}
|
|
|
|
func acquireNativeTransportConnection() (func(), bool) {
|
|
limit := nativeMaxConnectionLimit()
|
|
release, ok := acquireNativeCounter(&nativeTransportConnections, limit)
|
|
if !ok {
|
|
logNativeLimitRejection("transport connection", &nativeTransportRejected, limit)
|
|
}
|
|
return release, ok
|
|
}
|
|
|
|
func acquireNativeXHTTPRequest() (func(), bool) {
|
|
limit := nativeMaxXHTTPRequestLimit()
|
|
release, ok := acquireNativeCounter(&nativeXHTTPRequests, limit)
|
|
if !ok {
|
|
logNativeLimitRejection("XHTTP request", &nativeXHTTPRequestsRejected, limit)
|
|
}
|
|
return release, ok
|
|
}
|
|
|
|
func acquireNativeXHTTPSession() (func(), bool) {
|
|
limit := nativeXHTTPMaxSessionLimit()
|
|
release, ok := acquireNativeCounter(&nativeXHTTPSessions, limit)
|
|
if !ok {
|
|
logNativeLimitRejection("XHTTP session", &nativeXHTTPSessionsRejected, limit)
|
|
}
|
|
return release, ok
|
|
}
|
|
|
|
func configureNativeTransportSocket(c net.Conn) {
|
|
if tc, ok := c.(*net.TCPConn); ok {
|
|
_ = tc.SetKeepAlive(true)
|
|
_ = tc.SetKeepAlivePeriod(30 * time.Second)
|
|
_ = tc.SetNoDelay(true)
|
|
}
|
|
}
|
|
|
|
// nativeCountedConn releases its global transport slot and unregisters itself
|
|
// exactly once, even when several tunnel paths race to close the same socket.
|
|
type nativeCountedConn struct {
|
|
net.Conn
|
|
release func()
|
|
onClose func()
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
func (c *nativeCountedConn) Close() error {
|
|
c.closeOnce.Do(func() {
|
|
c.closeErr = c.Conn.Close()
|
|
if c.release != nil {
|
|
c.release()
|
|
}
|
|
if c.onClose != nil {
|
|
c.onClose()
|
|
}
|
|
})
|
|
return c.closeErr
|
|
}
|
|
|
|
// wrapNativeTransportConn applies only the global counter. It is useful for
|
|
// focused tests and for callers that own connection lifetime themselves.
|
|
func wrapNativeTransportConn(c net.Conn) (net.Conn, bool) {
|
|
if c == nil {
|
|
return nil, false
|
|
}
|
|
configureNativeTransportSocket(c)
|
|
release, ok := acquireNativeTransportConnection()
|
|
if !ok {
|
|
_ = c.Close()
|
|
return nil, false
|
|
}
|
|
return &nativeCountedConn{Conn: c, release: release}, true
|
|
}
|
|
|
|
// wrapTrackedNativeTransportConn additionally registers the accepted socket so
|
|
// stopping/restarting native Xray closes established raw, WebSocket, TLS, HTTP/1
|
|
// and HTTP/2 transports instead of leaving tunnel goroutines alive.
|
|
func wrapTrackedNativeTransportConn(c net.Conn) (net.Conn, bool) {
|
|
if c == nil {
|
|
return nil, false
|
|
}
|
|
configureNativeTransportSocket(c)
|
|
if !nativeTransportAccepting.Load() {
|
|
_ = c.Close()
|
|
return nil, false
|
|
}
|
|
release, ok := acquireNativeTransportConnection()
|
|
if !ok {
|
|
_ = c.Close()
|
|
return nil, false
|
|
}
|
|
|
|
counted := &nativeCountedConn{Conn: c, release: release}
|
|
counted.onClose = func() {
|
|
nativeTransportRegistry.Lock()
|
|
delete(nativeTransportRegistry.conns, counted)
|
|
nativeTransportRegistry.Unlock()
|
|
}
|
|
|
|
nativeTransportRegistry.Lock()
|
|
if !nativeTransportAccepting.Load() {
|
|
nativeTransportRegistry.Unlock()
|
|
_ = counted.Close()
|
|
return nil, false
|
|
}
|
|
nativeTransportRegistry.conns[counted] = struct{}{}
|
|
nativeTransportRegistry.Unlock()
|
|
return counted, true
|
|
}
|
|
|
|
func beginNativeTransportAccepting() {
|
|
nativeTransportAccepting.Store(true)
|
|
}
|
|
|
|
func stopNativeTransportAccepting() {
|
|
nativeTransportAccepting.Store(false)
|
|
}
|
|
|
|
func closeAllNativeTransportConnections() {
|
|
nativeTransportRegistry.Lock()
|
|
conns := make([]*nativeCountedConn, 0, len(nativeTransportRegistry.conns))
|
|
for c := range nativeTransportRegistry.conns {
|
|
conns = append(conns, c)
|
|
}
|
|
nativeTransportRegistry.Unlock()
|
|
for _, c := range conns {
|
|
_ = c.Close()
|
|
}
|
|
}
|
|
|
|
// nativeLimitedListener applies the same pre-authentication ceiling to XHTTP
|
|
// listeners. net/http receives only sockets that own a slot; rejected sockets
|
|
// are closed before it can allocate a per-connection goroutine or perform TLS.
|
|
type nativeLimitedListener struct {
|
|
net.Listener
|
|
}
|
|
|
|
func (l nativeLimitedListener) Accept() (net.Conn, error) {
|
|
for {
|
|
c, err := l.Listener.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if counted, ok := wrapTrackedNativeTransportConn(c); ok {
|
|
return counted, nil
|
|
}
|
|
// At the ceiling, a hot accept/close loop can itself consume a CPU core.
|
|
// A short fixed backoff also lets the kernel backlog absorb brief spikes.
|
|
time.Sleep(nativeOverloadBackoff)
|
|
}
|
|
}
|
|
|
|
func limitNativeListener(ln net.Listener) net.Listener {
|
|
if ln == nil {
|
|
return nil
|
|
}
|
|
return nativeLimitedListener{Listener: ln}
|
|
}
|