514 lines
13 KiB
Go
514 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
type xrayNativeQuotaState struct {
|
|
// trafficMu establishes a clean reset boundary. Native stream and packet
|
|
// writers hold a read lock from quota reservation through the actual write
|
|
// and metering; traffic resets take the write lock. This prevents an
|
|
// in-flight pre-reset reservation from being accounted in the new period or
|
|
// subtracting from freshly reset usage.
|
|
trafficMu sync.RWMutex
|
|
mu sync.Mutex
|
|
usedBytes int64
|
|
quotaBytes int64
|
|
action string
|
|
throttleMbps int
|
|
limiter *rate.Limiter
|
|
generation uint64
|
|
maxConns int
|
|
activeConns int
|
|
owner string
|
|
expiresAt time.Time
|
|
hasExpiry bool
|
|
connections map[io.Closer]struct{}
|
|
}
|
|
|
|
func (m *XrayManager) reloadNativeQuotaPolicies() {
|
|
if statsStore == nil {
|
|
return
|
|
}
|
|
metas, err := statsStore.ListAllXrayClients(context.Background())
|
|
if err != nil {
|
|
xrayLogf("xray native quota: load policies failed: %v", err)
|
|
return
|
|
}
|
|
next := make(map[string]*xrayNativeQuotaState, len(metas))
|
|
for _, meta := range metas {
|
|
if meta == nil || strings.TrimSpace(meta.UUID) == "" {
|
|
continue
|
|
}
|
|
next[meta.UUID] = newXrayNativeQuotaState(meta)
|
|
}
|
|
m.nativeQuotaMu.Lock()
|
|
m.nativeQuotaByUUID = next
|
|
m.nativeQuotaMu.Unlock()
|
|
}
|
|
|
|
func newXrayNativeQuotaState(meta *XrayClientMeta) *xrayNativeQuotaState {
|
|
used := meta.TotalUplinkBytes + meta.TotalDownlinkBytes
|
|
if used < 0 {
|
|
used = 0
|
|
}
|
|
return &xrayNativeQuotaState{
|
|
usedBytes: used,
|
|
quotaBytes: meta.DataQuotaBytes,
|
|
action: normalizeQuotaAction(meta.QuotaAction),
|
|
throttleMbps: quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps),
|
|
generation: 1,
|
|
maxConns: normalizeXrayMaxConns(meta.MaxConns),
|
|
owner: strings.TrimSpace(meta.OwnerUsername),
|
|
hasExpiry: meta.ExpiresAt != nil,
|
|
expiresAt: xrayExpiryValue(meta.ExpiresAt),
|
|
}
|
|
}
|
|
|
|
func xrayExpiryValue(expiry *time.Time) time.Time {
|
|
if expiry == nil {
|
|
return time.Time{}
|
|
}
|
|
return *expiry
|
|
}
|
|
|
|
func normalizeXrayMaxConns(v int) int {
|
|
if v < 0 {
|
|
return 0
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (m *XrayManager) setNativeQuotaPolicy(meta *XrayClientMeta) {
|
|
if meta == nil || strings.TrimSpace(meta.UUID) == "" {
|
|
return
|
|
}
|
|
uuid := strings.TrimSpace(meta.UUID)
|
|
m.nativeQuotaMu.Lock()
|
|
if m.nativeQuotaByUUID == nil {
|
|
m.nativeQuotaByUUID = make(map[string]*xrayNativeQuotaState)
|
|
}
|
|
existing := m.nativeQuotaByUUID[uuid]
|
|
if existing == nil {
|
|
m.nativeQuotaByUUID[uuid] = newXrayNativeQuotaState(meta)
|
|
m.nativeQuotaMu.Unlock()
|
|
return
|
|
}
|
|
m.nativeQuotaMu.Unlock()
|
|
|
|
existing.mu.Lock()
|
|
existing.quotaBytes = meta.DataQuotaBytes
|
|
existing.action = normalizeQuotaAction(meta.QuotaAction)
|
|
existing.throttleMbps = quotaThrottleMbpsOrDefault(meta.QuotaThrottleMbps)
|
|
existing.maxConns = normalizeXrayMaxConns(meta.MaxConns)
|
|
existing.owner = strings.TrimSpace(meta.OwnerUsername)
|
|
existing.hasExpiry = meta.ExpiresAt != nil
|
|
existing.expiresAt = xrayExpiryValue(meta.ExpiresAt)
|
|
existing.limiter = nil
|
|
existing.mu.Unlock()
|
|
}
|
|
|
|
func (m *XrayManager) removeNativeQuotaPolicy(uuid string) {
|
|
uuid = strings.TrimSpace(uuid)
|
|
if uuid == "" {
|
|
return
|
|
}
|
|
m.nativeQuotaMu.Lock()
|
|
state := m.nativeQuotaByUUID[uuid]
|
|
delete(m.nativeQuotaByUUID, uuid)
|
|
m.nativeQuotaMu.Unlock()
|
|
closeNativeClientConnections(state)
|
|
|
|
// Do not retain failed traffic/active deltas for a client that no longer
|
|
// exists. This also bounds the pending maps during a prolonged DB outage.
|
|
m.nativeDBMu.Lock()
|
|
delete(m.nativeTrafficPending, uuid)
|
|
delete(m.nativeActivePending, uuid)
|
|
m.nativeDBMu.Unlock()
|
|
}
|
|
|
|
func (m *XrayManager) resetNativeQuotaUsage(uuid string) {
|
|
uuid = strings.TrimSpace(uuid)
|
|
m.nativeQuotaMu.RLock()
|
|
state := m.nativeQuotaByUUID[uuid]
|
|
m.nativeQuotaMu.RUnlock()
|
|
if state == nil {
|
|
return
|
|
}
|
|
state.trafficMu.Lock()
|
|
defer state.trafficMu.Unlock()
|
|
state.mu.Lock()
|
|
state.usedBytes = 0
|
|
state.limiter = nil
|
|
state.generation++
|
|
if state.generation == 0 {
|
|
state.generation = 1
|
|
}
|
|
state.mu.Unlock()
|
|
}
|
|
|
|
func (m *XrayManager) resetNativeTrafficAccounting(ctx context.Context, store *Store, uuid, email string) error {
|
|
state := m.nativeQuotaState(uuid)
|
|
if state != nil {
|
|
state.trafficMu.Lock()
|
|
defer state.trafficMu.Unlock()
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
}
|
|
|
|
m.nativeTrafficPersistMu.Lock()
|
|
defer m.nativeTrafficPersistMu.Unlock()
|
|
|
|
// Remove this client's queued pre-reset delta while holding only the short
|
|
// map mutex. The database call may take seconds during an outage; keeping
|
|
// nativeDBMu locked across it would stall traffic/accounting updates for
|
|
// every other native user and could amplify a slow database into a goroutine
|
|
// pile-up.
|
|
m.nativeDBMu.Lock()
|
|
key := strings.TrimSpace(uuid)
|
|
var pending xrayPendingTraffic
|
|
hadPending := false
|
|
if m.nativeTrafficPending != nil {
|
|
pending, hadPending = m.nativeTrafficPending[key]
|
|
delete(m.nativeTrafficPending, key)
|
|
}
|
|
m.nativeDBMu.Unlock()
|
|
|
|
err := store.ResetXrayClientTraffic(ctx, uuid)
|
|
if err != nil && hadPending && pending.State == state {
|
|
m.nativeDBMu.Lock()
|
|
if m.nativeTrafficPending == nil {
|
|
m.nativeTrafficPending = make(map[string]xrayPendingTraffic)
|
|
}
|
|
current := m.nativeTrafficPending[key]
|
|
if current.State != nil && current.State != state {
|
|
current = xrayPendingTraffic{}
|
|
}
|
|
if current.Email == "" {
|
|
current.Email = pending.Email
|
|
}
|
|
current.Uplink += pending.Uplink
|
|
current.Downlink += pending.Downlink
|
|
current.State = state
|
|
m.nativeTrafficPending[key] = current
|
|
m.nativeDBMu.Unlock()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if state != nil {
|
|
state.usedBytes = 0
|
|
state.limiter = nil
|
|
state.generation++
|
|
if state.generation == 0 {
|
|
state.generation = 1
|
|
}
|
|
}
|
|
|
|
m.statsMu.Lock()
|
|
for _, key := range []string{strings.TrimSpace(email), strings.TrimSpace(uuid)} {
|
|
if key == "" {
|
|
continue
|
|
}
|
|
if runtime, ok := m.statsByEmail[key]; ok {
|
|
runtime.Uplink = 0
|
|
runtime.Downlink = 0
|
|
m.statsByEmail[key] = runtime
|
|
}
|
|
}
|
|
m.statsMu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (m *XrayManager) nativeQuotaState(uuid string) *xrayNativeQuotaState {
|
|
m.nativeQuotaMu.RLock()
|
|
state := m.nativeQuotaByUUID[strings.TrimSpace(uuid)]
|
|
m.nativeQuotaMu.RUnlock()
|
|
return state
|
|
}
|
|
|
|
// acquireNativeClientConnection enforces the DB-backed max_conns policy across
|
|
// every native inbound and transport. The returned release function is safe to
|
|
// call more than once and keeps runtime/DB online counters in sync.
|
|
func (m *XrayManager) acquireNativeClientConnection(uuid, email string, closers ...io.Closer) (func(), *xrayNativeQuotaState, bool) {
|
|
state := m.nativeQuotaState(uuid)
|
|
var closer io.Closer
|
|
if len(closers) > 0 {
|
|
closer = closers[0]
|
|
}
|
|
if state != nil {
|
|
state.mu.Lock()
|
|
if reason := nativeClientAccessDeniedLocked(state); reason != "" {
|
|
state.mu.Unlock()
|
|
xrayLogf("native xray: rejected user %s: %s", email, reason)
|
|
return nil, state, false
|
|
}
|
|
if state.maxConns > 0 && state.activeConns >= state.maxConns {
|
|
limit := state.maxConns
|
|
state.mu.Unlock()
|
|
logNativeClientLimitRejection(email, limit)
|
|
return nil, state, false
|
|
}
|
|
state.activeConns++
|
|
if closer != nil {
|
|
if state.connections == nil {
|
|
state.connections = make(map[io.Closer]struct{})
|
|
}
|
|
state.connections[closer] = struct{}{}
|
|
}
|
|
state.mu.Unlock()
|
|
}
|
|
|
|
m.recordNativeConnect(uuid, email, state)
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
if state != nil {
|
|
state.mu.Lock()
|
|
if closer != nil {
|
|
delete(state.connections, closer)
|
|
}
|
|
if state.activeConns > 0 {
|
|
state.activeConns--
|
|
}
|
|
state.mu.Unlock()
|
|
}
|
|
m.recordNativeDisconnect(uuid, email, state)
|
|
})
|
|
}, state, true
|
|
}
|
|
|
|
func closeNativeClientConnections(state *xrayNativeQuotaState) {
|
|
if state == nil {
|
|
return
|
|
}
|
|
state.mu.Lock()
|
|
closers := make([]io.Closer, 0, len(state.connections))
|
|
for closer := range state.connections {
|
|
closers = append(closers, closer)
|
|
}
|
|
state.mu.Unlock()
|
|
for _, closer := range closers {
|
|
_ = closer.Close()
|
|
}
|
|
}
|
|
|
|
func (m *XrayManager) disconnectNativeClient(uuid string) {
|
|
closeNativeClientConnections(m.nativeQuotaState(uuid))
|
|
}
|
|
|
|
func (m *XrayManager) nativeClientAccessDenied(uuid string) string {
|
|
state := m.nativeQuotaState(uuid)
|
|
if state == nil {
|
|
return ""
|
|
}
|
|
state.mu.Lock()
|
|
reason := nativeClientAccessDeniedLocked(state)
|
|
state.mu.Unlock()
|
|
return reason
|
|
}
|
|
|
|
func nativeClientAccessDeniedLocked(state *xrayNativeQuotaState) string {
|
|
if state.hasExpiry && !state.expiresAt.After(time.Now()) {
|
|
return "expired"
|
|
}
|
|
if state.owner != "" {
|
|
if err := ownerIsActive(state.owner); err != nil {
|
|
return "owner suspended or expired"
|
|
}
|
|
}
|
|
if state.quotaBytes > 0 && normalizeQuotaAction(state.action) == quotaActionBlock && state.usedBytes >= state.quotaBytes {
|
|
return "data quota exceeded"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (m *XrayManager) nativeQuotaBlocked(uuid string) bool {
|
|
return nativeQuotaStateBlocked(m.nativeQuotaState(uuid))
|
|
}
|
|
|
|
func nativeQuotaStateBlocked(state *xrayNativeQuotaState) bool {
|
|
if state == nil {
|
|
return false
|
|
}
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
return state.quotaBytes > 0 && normalizeQuotaAction(state.action) == quotaActionBlock && state.usedBytes >= state.quotaBytes
|
|
}
|
|
|
|
func (m *XrayManager) reserveNativeQuota(state *xrayNativeQuotaState, requested int) (allowed int, limiter *rate.Limiter, stopAfter bool) {
|
|
if requested <= 0 {
|
|
return 0, nil, false
|
|
}
|
|
if state == nil {
|
|
return requested, nil, false
|
|
}
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
|
|
n := int64(requested)
|
|
if state.quotaBytes <= 0 {
|
|
state.usedBytes += n
|
|
return requested, nil, false
|
|
}
|
|
if normalizeQuotaAction(state.action) == quotaActionThrottle {
|
|
previous := state.usedBytes
|
|
state.usedBytes += n
|
|
if previous+n > state.quotaBytes {
|
|
if state.limiter == nil {
|
|
bps := mbpsToBytesPerSec(quotaThrottleMbpsOrDefault(state.throttleMbps))
|
|
burst := int(bps)
|
|
if burst < copyBufSize {
|
|
burst = copyBufSize
|
|
}
|
|
state.limiter = rate.NewLimiter(rate.Limit(bps), burst)
|
|
}
|
|
return requested, state.limiter, false
|
|
}
|
|
return requested, nil, false
|
|
}
|
|
|
|
remaining := state.quotaBytes - state.usedBytes
|
|
if remaining <= 0 {
|
|
return 0, nil, true
|
|
}
|
|
take := n
|
|
if take > remaining {
|
|
take = remaining
|
|
}
|
|
state.usedBytes += take
|
|
return int(take), nil, take < n
|
|
}
|
|
|
|
func (m *XrayManager) finishNativeQuotaReservation(state *xrayNativeQuotaState, reserved, written int) {
|
|
if reserved <= 0 || written >= reserved {
|
|
return
|
|
}
|
|
if written < 0 {
|
|
written = 0
|
|
}
|
|
if state == nil {
|
|
return
|
|
}
|
|
state.mu.Lock()
|
|
state.usedBytes -= int64(reserved - written)
|
|
if state.usedBytes < 0 {
|
|
state.usedBytes = 0
|
|
}
|
|
state.mu.Unlock()
|
|
}
|
|
|
|
type xrayQuotaMeteredWriter struct {
|
|
w io.Writer
|
|
meter *trafficMeter
|
|
ctx context.Context
|
|
}
|
|
|
|
func (mw xrayQuotaMeteredWriter) Write(p []byte) (int, error) {
|
|
if mw.meter == nil {
|
|
return mw.w.Write(p)
|
|
}
|
|
state := mw.meter.state
|
|
if state != nil {
|
|
state.trafficMu.RLock()
|
|
defer state.trafficMu.RUnlock()
|
|
}
|
|
allowed, limiter, stopAfter := xrayMgr.reserveNativeQuota(state, len(p))
|
|
if allowed <= 0 {
|
|
return 0, errDataQuotaExceeded
|
|
}
|
|
if limiter != nil {
|
|
ctx := mw.ctx
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := limiter.WaitN(ctx, allowed); err != nil {
|
|
xrayMgr.finishNativeQuotaReservation(state, allowed, 0)
|
|
return 0, err
|
|
}
|
|
}
|
|
n, err := mw.w.Write(p[:allowed])
|
|
xrayMgr.finishNativeQuotaReservation(state, allowed, n)
|
|
if n > 0 {
|
|
mw.meter.add(n)
|
|
}
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
if stopAfter || allowed < len(p) || nativeQuotaStateBlocked(state) {
|
|
return n, errDataQuotaExceeded
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
type nativePacketQuotaReservation struct {
|
|
meter *trafficMeter
|
|
state *xrayNativeQuotaState
|
|
limiter *rate.Limiter
|
|
reserved int
|
|
finished bool
|
|
}
|
|
|
|
func reserveNativePacketQuota(meter *trafficMeter, n int) (nativePacketQuotaReservation, error) {
|
|
if meter == nil || n <= 0 {
|
|
return nativePacketQuotaReservation{}, nil
|
|
}
|
|
state := meter.state
|
|
if state != nil {
|
|
state.trafficMu.RLock()
|
|
}
|
|
allowed, limiter, stopAfter := xrayMgr.reserveNativeQuota(state, n)
|
|
if allowed != n || stopAfter {
|
|
if allowed > 0 {
|
|
xrayMgr.finishNativeQuotaReservation(state, allowed, 0)
|
|
}
|
|
if state != nil {
|
|
state.trafficMu.RUnlock()
|
|
}
|
|
return nativePacketQuotaReservation{}, errDataQuotaExceeded
|
|
}
|
|
return nativePacketQuotaReservation{
|
|
meter: meter,
|
|
state: state,
|
|
limiter: limiter,
|
|
reserved: n,
|
|
}, nil
|
|
}
|
|
|
|
func (r *nativePacketQuotaReservation) wait(ctx context.Context) error {
|
|
if r == nil || r.finished || r.limiter == nil || r.reserved <= 0 {
|
|
return nil
|
|
}
|
|
if err := waitNativeRate(ctx, r.limiter, r.reserved); err != nil {
|
|
r.finish(0)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *nativePacketQuotaReservation) finish(written int) {
|
|
if r == nil || r.finished {
|
|
return
|
|
}
|
|
r.finished = true
|
|
if r.meter == nil || r.reserved <= 0 {
|
|
if r.state != nil {
|
|
r.state.trafficMu.RUnlock()
|
|
}
|
|
return
|
|
}
|
|
xrayMgr.finishNativeQuotaReservation(r.state, r.reserved, written)
|
|
if written > 0 {
|
|
r.meter.add(written)
|
|
}
|
|
if r.state != nil {
|
|
r.state.trafficMu.RUnlock()
|
|
}
|
|
}
|