810 lines
24 KiB
Go
810 lines
24 KiB
Go
package xray
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"crypto/sha256"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/xtls/xray-core/common/protocol"
|
||
"github.com/xtls/xray-core/common/uuid"
|
||
xrayCore "github.com/xtls/xray-core/core"
|
||
"github.com/xtls/xray-core/features/inbound"
|
||
"github.com/xtls/xray-core/features/stats"
|
||
featurebandwidth "github.com/xtls/xray-core/features/bandwidth"
|
||
"github.com/xtls/xray-core/infra/conf/serial"
|
||
xrayProxy "github.com/xtls/xray-core/proxy"
|
||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||
ss2022 "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||
"github.com/xtls/xray-core/proxy/trojan"
|
||
"github.com/xtls/xray-core/proxy/vless"
|
||
"github.com/xtls/xray-core/proxy/vmess"
|
||
"golang.org/x/time/rate"
|
||
|
||
_ "github.com/xtls/xray-core/main/distro/all"
|
||
|
||
"github.com/cedar2025/xboard-node/internal/config"
|
||
"github.com/cedar2025/xboard-node/internal/kernel"
|
||
"github.com/cedar2025/xboard-node/internal/kernel/geodata"
|
||
"github.com/cedar2025/xboard-node/internal/nlog"
|
||
"github.com/cedar2025/xboard-node/internal/model"
|
||
)
|
||
|
||
const (
|
||
// drainTimeout is how long Stop waits for in-flight connections to finish
|
||
// before hard-killing the instance. Skipped during hot-reload for speed.
|
||
drainTimeout = 5 * time.Second
|
||
// startTimeout caps how long instance.Start() may block.
|
||
startTimeout = 30 * time.Second
|
||
)
|
||
|
||
// Xray implements kernel.Kernel by embedding xray-core as a Go library.
|
||
//
|
||
// Lifecycle: Start → running → Stop / Reload
|
||
//
|
||
// Lock discipline:
|
||
// - mu is held only for brief field swaps (microseconds).
|
||
// - instance.Start() and instance.Close() run OUTSIDE the lock.
|
||
// - running (atomic) gates fast-path checks in IsRunning / GetConnections.
|
||
type Xray struct {
|
||
cfg config.KernelConfig
|
||
|
||
// mu protects instance, limitDispatcher, users, protocol, inboundTag,
|
||
// lastKernelHash, and cumTraffic. Never held during slow I/O.
|
||
mu sync.Mutex
|
||
instance *xrayCore.Instance
|
||
limitDispatcher *LimitDispatcher
|
||
users []model.UserSpec
|
||
nodeConfig *model.NodeSpec
|
||
tls kernel.TLSCert
|
||
protocol string
|
||
inboundTag string
|
||
lastKernelHash string
|
||
cumTraffic map[int][2]int64
|
||
speedLimitFunc func(string) *rate.Limiter
|
||
|
||
// running is set after a successful Start and cleared before shutdown.
|
||
// Atomic so IsRunning / GetConnections never block.
|
||
running atomic.Bool
|
||
}
|
||
|
||
func New(cfg config.KernelConfig) *Xray {
|
||
return &Xray{
|
||
cfg: cfg,
|
||
cumTraffic: make(map[int][2]int64),
|
||
}
|
||
}
|
||
|
||
func (x *Xray) Name() string { return "xray" }
|
||
|
||
func (x *Xray) Capabilities() kernel.Capabilities {
|
||
return kernel.Capabilities{
|
||
PerUserSpeedLimit: true,
|
||
DeviceLimit: true,
|
||
BuiltInTrafficStats: true,
|
||
AliveIPTracking: true,
|
||
ForceCloseConnection: false,
|
||
ForceCloseUser: false,
|
||
}
|
||
}
|
||
|
||
func (x *Xray) Protocols() []string {
|
||
return []string{
|
||
"vmess", "vless", "trojan", "shadowsocks",
|
||
"hysteria",
|
||
}
|
||
}
|
||
|
||
// ─── Lifecycle ──────────────────────────────────────────────────────────────
|
||
|
||
// Start builds a new xray-core instance and atomically replaces the old
|
||
// one. The method is organised in five non-overlapping phases so that the
|
||
// kernel mutex is never held during slow operations (Start / Close).
|
||
// Crucially, the old instance stays alive until the new one is confirmed
|
||
// running — if StartNew fails, the old instance is untouched.
|
||
//
|
||
// Phase 1 – Build: generate protobuf config (no lock, pure computation)
|
||
// Phase 2 – Create: xrayCore.New + capture LD (brief global lock)
|
||
// Phase 3 – StartNew: instance.Start (no lock, potentially slow)
|
||
// Phase 4 – Swap: store new, extract old (brief kernel lock)
|
||
// Phase 5 – RecycleOld: close old in background (non-blocking)
|
||
func (x *Xray) Start(nodeConfig *model.NodeSpec, users []model.UserSpec, tls kernel.TLSCert) error {
|
||
// ── Phase 1: Build config (no shared state) ─────────────────────────
|
||
x.ensureGeoData(nodeConfig)
|
||
|
||
data, err := marshalConfig(x.cfg, nodeConfig, users, tls)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
pbConfig, err := serial.LoadJSONConfig(bytes.NewReader(data))
|
||
if err != nil {
|
||
return fmt.Errorf("parse xray config: %w", err)
|
||
}
|
||
|
||
// ── Phase 2: Create instance (global lock for LD capture) ───────────
|
||
xrayCreationMu.Lock()
|
||
inst, err := xrayCore.New(pbConfig)
|
||
ld := globalLimitDispatcher.Load()
|
||
xrayCreationMu.Unlock()
|
||
if err != nil {
|
||
return fmt.Errorf("create xray: %w", err)
|
||
}
|
||
|
||
// ── Phase 3: Start new (no lock, potentially slow) ──────────────────
|
||
if err := startWithTimeout(inst, startTimeout); err != nil {
|
||
inst.Close()
|
||
return err
|
||
}
|
||
|
||
// ── Phase 4: Swap old → new (brief kernel lock) ─────────────────────
|
||
x.mu.Lock()
|
||
old := x.instance
|
||
oldLD := x.limitDispatcher
|
||
x.instance = inst
|
||
x.limitDispatcher = ld
|
||
x.users = users
|
||
x.nodeConfig = nodeConfig
|
||
x.tls = tls
|
||
x.protocol = nodeConfig.Protocol
|
||
x.inboundTag = nodeConfig.Protocol + "-in"
|
||
x.cumTraffic = make(map[int][2]int64)
|
||
x.lastKernelHash = kernel.ComputeHash(nodeConfig, users)
|
||
x.running.Store(true)
|
||
x.mu.Unlock()
|
||
|
||
// ── Phase 5: Recycle old (background, non-blocking) ─────────────────
|
||
closeOld(old, oldLD)
|
||
|
||
x.updateDispatcherLimits(users)
|
||
x.updateBandwidthLimits(users)
|
||
|
||
nlog.Core().Info("xray started",
|
||
"users", len(users),
|
||
"protocol", nodeConfig.Protocol,
|
||
)
|
||
return nil
|
||
}
|
||
|
||
// Reload updates dispatcher limits and handles configuration changes.
|
||
// Since Xray does not support granular inbound reconstruction without an
|
||
// instance restart for most transport/TLS settings, it triggers a full restart
|
||
// if any kernel-affecting fields (hash mismatch) have changed.
|
||
func (x *Xray) Reload(nodeConfig *model.NodeSpec, users []model.UserSpec, tls kernel.TLSCert) error {
|
||
x.updateDispatcherLimits(users)
|
||
x.updateBandwidthLimits(users)
|
||
|
||
newHash := kernel.ComputeHash(nodeConfig, users)
|
||
|
||
x.mu.Lock()
|
||
same := x.lastKernelHash == newHash && tlsEqual(x.tls, tls)
|
||
if same {
|
||
x.users = users
|
||
x.mu.Unlock()
|
||
nlog.Core().Debug("xray: limits updated, kernel configuration unchanged")
|
||
return nil
|
||
}
|
||
x.mu.Unlock()
|
||
|
||
nlog.Core().Info("xray: configuration changed, performing full restart")
|
||
return x.Start(nodeConfig, users, tls)
|
||
}
|
||
|
||
// Stop gracefully shuts down the kernel, draining active connections first.
|
||
func (x *Xray) Stop() {
|
||
x.running.Store(false)
|
||
|
||
x.mu.Lock()
|
||
inst := x.instance
|
||
ld := x.limitDispatcher
|
||
x.instance = nil
|
||
x.limitDispatcher = nil
|
||
x.mu.Unlock()
|
||
|
||
if ld != nil {
|
||
drainConns(ld, drainTimeout)
|
||
}
|
||
closeOld(inst, ld)
|
||
}
|
||
|
||
func (x *Xray) IsRunning() bool { return x.running.Load() }
|
||
|
||
func tlsEqual(a, b kernel.TLSCert) bool {
|
||
return bytes.Equal(a.CertPEM, b.CertPEM) && bytes.Equal(a.KeyPEM, b.KeyPEM)
|
||
}
|
||
|
||
// ─── Observability ──────────────────────────────────────────────────────────
|
||
|
||
// GetUserTraffic returns per-user cumulative traffic from xray's built-in
|
||
// stats pipeline and connection state from the admission dispatcher when
|
||
// available. The dispatcher intentionally no longer wraps transport links,
|
||
// so traffic accounting must come from xray-core itself.
|
||
func (x *Xray) GetUserTraffic(_ context.Context) (traffic map[int][2]int64, aliveIPs map[int]map[string]bool, connCount int, err error) {
|
||
if !x.running.Load() {
|
||
return nil, nil, 0, nil
|
||
}
|
||
|
||
x.mu.Lock()
|
||
ld := x.limitDispatcher
|
||
traffic, err = x.aggregateStats()
|
||
x.mu.Unlock()
|
||
if err != nil {
|
||
return nil, nil, 0, err
|
||
}
|
||
|
||
if ld != nil {
|
||
aliveIPs, connCount = ld.GetConnectionState()
|
||
}
|
||
return traffic, aliveIPs, connCount, nil
|
||
}
|
||
|
||
func (x *Xray) CloseConnection(_ context.Context, _ string) error {
|
||
// No-op: xray doesn't support force-closing individual connections.
|
||
// User removal goes through RemoveUsers which removes the inbound user.
|
||
return nil
|
||
}
|
||
|
||
func (x *Xray) CloseUserConnections(_ context.Context, _ string) error {
|
||
// No-op: handled by RemoveUsers at the xray core level.
|
||
return nil
|
||
}
|
||
|
||
// SetSpeedLimitFunc wires xray's patched bandwidth feature to the shared
|
||
// per-user limiter callback used by the service layer. Unlike the old no-op
|
||
// behavior, xray now consumes this callback to support dynamic speed updates.
|
||
func (x *Xray) SetSpeedLimitFunc(fn func(string) *rate.Limiter) {
|
||
x.mu.Lock()
|
||
x.speedLimitFunc = fn
|
||
x.mu.Unlock()
|
||
x.updateBandwidthLimits(nil)
|
||
}
|
||
|
||
// SetDeviceLimitFunc is a no-op for xray — device limits are already
|
||
// gate-kept by LimitDispatcher.checkDeviceLimit at Dispatch time.
|
||
func (x *Xray) SetDeviceLimitFunc(_ func(string) (int, bool)) {}
|
||
|
||
// UpdateGlobalDevices is a no-op for xray — xray handles device limits differently.
|
||
func (x *Xray) UpdateGlobalDevices(_ map[int][]string) {}
|
||
|
||
// ClearGlobalDevices is a no-op for xray.
|
||
func (x *Xray) ClearGlobalDevices() {}
|
||
|
||
// ─── User management (non-disruptive where possible) ────────────────────────
|
||
|
||
// AddUsers adds new users to the running kernel via xray's UserManager API.
|
||
// For protocols that support UserManager (vmess, vless, trojan, shadowsocks),
|
||
// this is truly hitless — no restart, no connection disruption.
|
||
// For unsupported protocols (socks, http), falls back to full restart.
|
||
//
|
||
// Delta "add" events also carry property updates (speed/device limits) for
|
||
// existing users, so this method always merges properties and refreshes the
|
||
// dispatcher limits — even when no brand-new users need to be added.
|
||
func (x *Xray) AddUsers(users []model.UserSpec) (int, error) {
|
||
x.mu.Lock()
|
||
if x.instance == nil {
|
||
x.mu.Unlock()
|
||
return 0, fmt.Errorf("not running")
|
||
}
|
||
|
||
// Merge: overwrite existing users' properties, collect truly new ones.
|
||
userMap := make(map[int]model.UserSpec, len(x.users))
|
||
for _, u := range x.users {
|
||
userMap[u.ID] = u
|
||
}
|
||
var toAdd []model.UserSpec
|
||
for _, u := range users {
|
||
if _, exists := userMap[u.ID]; !exists {
|
||
toAdd = append(toAdd, u)
|
||
}
|
||
userMap[u.ID] = u // always overwrite properties
|
||
}
|
||
merged := make([]model.UserSpec, 0, len(userMap))
|
||
for _, u := range userMap {
|
||
merged = append(merged, u)
|
||
}
|
||
|
||
if len(toAdd) == 0 {
|
||
// No new kernel users, but properties (limits) may have changed.
|
||
x.users = merged
|
||
x.mu.Unlock()
|
||
x.updateDispatcherLimits(merged)
|
||
x.updateBandwidthLimits(merged)
|
||
return 0, nil
|
||
}
|
||
|
||
um, err := x.getUserManager()
|
||
if err != nil {
|
||
// Protocol doesn't support UserManager → full restart
|
||
nc, t := x.nodeConfig, x.tls
|
||
x.mu.Unlock()
|
||
nlog.Core().Debug("xray: AddUsers fallback to restart", "reason", err)
|
||
if err := x.Start(nc, merged, t); err != nil {
|
||
return 0, err
|
||
}
|
||
return len(toAdd), nil
|
||
}
|
||
|
||
proto := x.protocol
|
||
nc := x.nodeConfig
|
||
x.mu.Unlock()
|
||
|
||
ctx := context.Background()
|
||
added := 0
|
||
for _, u := range toAdd {
|
||
mu, err := toMemoryUser(proto, nc, u)
|
||
if err != nil {
|
||
nlog.Core().Warn("xray: skip user, cannot build account", "user", u.ID, "error", err)
|
||
continue
|
||
}
|
||
if err := um.AddUser(ctx, mu); err != nil {
|
||
nlog.Core().Warn("xray: AddUser failed", "user", u.ID, "error", err)
|
||
continue
|
||
}
|
||
added++
|
||
}
|
||
|
||
// Update bookkeeping with full merged list (new users + updated properties).
|
||
x.mu.Lock()
|
||
x.users = merged
|
||
x.mu.Unlock()
|
||
x.updateDispatcherLimits(merged)
|
||
x.updateBandwidthLimits(merged)
|
||
|
||
nlog.Core().Info("xray: users added via UserManager", "added", added, "total", len(merged))
|
||
return added, nil
|
||
}
|
||
|
||
// RemoveUsers removes users from the running kernel via xray's UserManager API.
|
||
// Truly hitless for supported protocols — remaining connections unaffected.
|
||
func (x *Xray) RemoveUsers(users []model.UserSpec) (int, error) {
|
||
x.mu.Lock()
|
||
if x.instance == nil {
|
||
x.mu.Unlock()
|
||
return 0, fmt.Errorf("not running")
|
||
}
|
||
removeSet := make(map[int]struct{}, len(users))
|
||
for _, u := range users {
|
||
removeSet[u.ID] = struct{}{}
|
||
}
|
||
var kept []model.UserSpec
|
||
removed := 0
|
||
for _, u := range x.users {
|
||
if _, rm := removeSet[u.ID]; rm {
|
||
removed++
|
||
} else {
|
||
kept = append(kept, u)
|
||
}
|
||
}
|
||
if removed == 0 {
|
||
x.mu.Unlock()
|
||
return 0, nil
|
||
}
|
||
|
||
if len(kept) == 0 {
|
||
x.mu.Unlock()
|
||
x.Stop()
|
||
return removed, nil
|
||
}
|
||
|
||
um, err := x.getUserManager()
|
||
if err != nil {
|
||
nc, t := x.nodeConfig, x.tls
|
||
x.mu.Unlock()
|
||
nlog.Core().Debug("xray: RemoveUsers fallback to restart", "reason", err)
|
||
if err := x.Start(nc, kept, t); err != nil {
|
||
return 0, err
|
||
}
|
||
return removed, nil
|
||
}
|
||
x.mu.Unlock()
|
||
|
||
ctx := context.Background()
|
||
actualRemoved := 0
|
||
for _, u := range users {
|
||
email := userEmail(u.ID)
|
||
if err := um.RemoveUser(ctx, email); err != nil {
|
||
nlog.Core().Debug("xray: RemoveUser skipped", "user", u.ID, "error", err)
|
||
continue
|
||
}
|
||
actualRemoved++
|
||
}
|
||
|
||
x.mu.Lock()
|
||
x.users = kept
|
||
x.mu.Unlock()
|
||
x.updateDispatcherLimits(kept)
|
||
x.updateBandwidthLimits(kept)
|
||
|
||
nlog.Core().Info("xray: users removed via UserManager", "removed", actualRemoved, "total", len(kept))
|
||
return actualRemoved, nil
|
||
}
|
||
|
||
// UpdateUsers replaces the entire user set. If only speed/device limits
|
||
// changed, updates the dispatcher without restarting. Otherwise uses
|
||
// UserManager for hitless add/remove where supported.
|
||
func (x *Xray) UpdateUsers(users []model.UserSpec) (added, removed int, err error) {
|
||
x.mu.Lock()
|
||
if x.instance == nil {
|
||
x.mu.Unlock()
|
||
return 0, 0, fmt.Errorf("not running")
|
||
}
|
||
toAdd, toRemove := kernel.UserDiff(x.users, users)
|
||
added, removed = len(toAdd), len(toRemove)
|
||
|
||
if added == 0 && removed == 0 {
|
||
// Only limits changed — update dispatcher without restart.
|
||
x.users = users
|
||
x.mu.Unlock()
|
||
x.updateDispatcherLimits(users)
|
||
x.updateBandwidthLimits(users)
|
||
nlog.Core().Info("xray: limits updated, kernel unchanged")
|
||
return 0, 0, nil
|
||
}
|
||
|
||
um, umErr := x.getUserManager()
|
||
if umErr != nil {
|
||
// Protocol doesn't support UserManager → full restart
|
||
nc, t := x.nodeConfig, x.tls
|
||
x.mu.Unlock()
|
||
nlog.Core().Debug("xray: UpdateUsers fallback to restart", "reason", umErr)
|
||
if err = x.Start(nc, users, t); err != nil {
|
||
return 0, 0, err
|
||
}
|
||
return
|
||
}
|
||
|
||
proto := x.protocol
|
||
nc := x.nodeConfig
|
||
x.mu.Unlock()
|
||
|
||
ctx := context.Background()
|
||
|
||
// Remove first, then add (order matters for UUID changes on same ID)
|
||
for _, u := range toRemove {
|
||
email := userEmail(u.ID)
|
||
if err := um.RemoveUser(ctx, email); err != nil {
|
||
nlog.Core().Debug("xray: RemoveUser skipped in UpdateUsers", "user", u.ID, "error", err)
|
||
}
|
||
}
|
||
for _, u := range toAdd {
|
||
mu, err := toMemoryUser(proto, nc, u)
|
||
if err != nil {
|
||
nlog.Core().Warn("xray: skip user in UpdateUsers", "user", u.ID, "error", err)
|
||
continue
|
||
}
|
||
if err := um.AddUser(ctx, mu); err != nil {
|
||
nlog.Core().Warn("xray: AddUser failed in UpdateUsers", "user", u.ID, "error", err)
|
||
}
|
||
}
|
||
|
||
x.mu.Lock()
|
||
x.users = users
|
||
x.mu.Unlock()
|
||
x.updateDispatcherLimits(users)
|
||
x.updateBandwidthLimits(users)
|
||
|
||
nlog.Core().Info("xray: users updated via UserManager", "added", added, "removed", removed, "total", len(users))
|
||
return
|
||
}
|
||
|
||
var _ kernel.Kernel = (*Xray)(nil)
|
||
|
||
// ─── Internal helpers ───────────────────────────────────────────────────────
|
||
|
||
// getUserManager extracts the UserManager from the running xray instance's
|
||
// inbound handler. Must be called with x.mu held.
|
||
// Returns error for protocols that don't support UserManager (socks, http).
|
||
func (x *Xray) getUserManager() (xrayProxy.UserManager, error) {
|
||
if x.instance == nil {
|
||
return nil, fmt.Errorf("instance is nil")
|
||
}
|
||
|
||
im := x.instance.GetFeature(inbound.ManagerType())
|
||
if im == nil {
|
||
return nil, fmt.Errorf("inbound manager not available")
|
||
}
|
||
inboundMgr, ok := im.(inbound.Manager)
|
||
if !ok {
|
||
return nil, fmt.Errorf("inbound manager type assertion failed")
|
||
}
|
||
|
||
handler, err := inboundMgr.GetHandler(context.Background(), x.inboundTag)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("get handler %q: %w", x.inboundTag, err)
|
||
}
|
||
|
||
gi, ok := handler.(xrayProxy.GetInbound)
|
||
if !ok {
|
||
return nil, fmt.Errorf("handler does not implement GetInbound")
|
||
}
|
||
|
||
um, ok := gi.GetInbound().(xrayProxy.UserManager)
|
||
if !ok {
|
||
return nil, fmt.Errorf("protocol %q does not support UserManager", x.protocol)
|
||
}
|
||
|
||
return um, nil
|
||
}
|
||
|
||
// toMemoryUser converts a model.UserSpec into an xray protocol.MemoryUser for the
|
||
// given protocol. Each protocol needs a different Account type.
|
||
func toMemoryUser(proto string, nc *model.NodeSpec, u model.UserSpec) (*protocol.MemoryUser, error) {
|
||
email := userEmail(u.ID)
|
||
mu := &protocol.MemoryUser{Email: email, Level: 0}
|
||
|
||
switch proto {
|
||
case "vmess":
|
||
id, err := uuid.ParseString(u.UUID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse vmess UUID: %w", err)
|
||
}
|
||
mu.Account = &vmess.MemoryAccount{
|
||
ID: protocol.NewID(id),
|
||
Security: protocol.SecurityType_AUTO,
|
||
}
|
||
|
||
case "vless":
|
||
id, err := uuid.ParseString(u.UUID)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("parse vless UUID: %w", err)
|
||
}
|
||
account := &vless.MemoryAccount{
|
||
ID: protocol.NewID(id),
|
||
Encryption: "none",
|
||
}
|
||
if nc.Flow != "" {
|
||
account.Flow = nc.Flow
|
||
}
|
||
mu.Account = account
|
||
|
||
case "trojan":
|
||
mu.Account = &trojan.MemoryAccount{
|
||
Password: u.UUID,
|
||
Key: trojanHexSha224(u.UUID),
|
||
}
|
||
|
||
case "shadowsocks":
|
||
if strings.HasPrefix(nc.Cipher, "2022-blake3-") {
|
||
// 2022-blake3 multi-user mode
|
||
mu.Account = &ss2022.MemoryAccount{Key: u.UUID}
|
||
} else {
|
||
// Traditional SS — build via getCipher path
|
||
ct := parseCipherType(nc.Cipher)
|
||
cipherObj, err := (&shadowsocks.Account{CipherType: ct, Password: u.UUID}).AsAccount()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("build ss account: %w", err)
|
||
}
|
||
mu.Account = cipherObj
|
||
}
|
||
|
||
default:
|
||
return nil, fmt.Errorf("protocol %q does not support MemoryUser", proto)
|
||
}
|
||
|
||
return mu, nil
|
||
}
|
||
|
||
// parseCipherType maps a cipher name string to the xray CipherType enum.
|
||
func parseCipherType(cipher string) shadowsocks.CipherType {
|
||
switch cipher {
|
||
case "aes-128-gcm":
|
||
return shadowsocks.CipherType_AES_128_GCM
|
||
case "aes-256-gcm":
|
||
return shadowsocks.CipherType_AES_256_GCM
|
||
case "chacha20-ietf-poly1305", "chacha20-poly1305":
|
||
return shadowsocks.CipherType_CHACHA20_POLY1305
|
||
case "xchacha20-ietf-poly1305", "xchacha20-poly1305":
|
||
return shadowsocks.CipherType_XCHACHA20_POLY1305
|
||
case "none", "plain":
|
||
return shadowsocks.CipherType_NONE
|
||
default:
|
||
return shadowsocks.CipherType_AES_256_GCM
|
||
}
|
||
}
|
||
|
||
// trojanHexSha224 computes the SHA-224 hex key used by trojan protocol.
|
||
func trojanHexSha224(password string) []byte {
|
||
// Replicates the exact logic from xray-core/proxy/trojan/config.go hexSha224
|
||
h := sha256.New224()
|
||
h.Write([]byte(password))
|
||
buf := make([]byte, 56)
|
||
hexEncode(buf, h.Sum(nil))
|
||
return buf
|
||
}
|
||
|
||
// hexEncode is a minimal hex encoder (avoids importing encoding/hex).
|
||
func hexEncode(dst, src []byte) {
|
||
const hextable = "0123456789abcdef"
|
||
for i, v := range src {
|
||
dst[i*2] = hextable[v>>4]
|
||
dst[i*2+1] = hextable[v&0x0f]
|
||
}
|
||
}
|
||
|
||
// ensureGeoData downloads geo databases when routes reference geoip/geosite.
|
||
func (x *Xray) ensureGeoData(nc *model.NodeSpec) {
|
||
needIP, needSite := kernel.NeedsGeoIP(nc.Routes), kernel.NeedsGeoSite(nc.Routes)
|
||
if !needIP && !needSite {
|
||
return
|
||
}
|
||
dir := x.cfg.GeoDataDir
|
||
if err := geodata.Ensure(dir, needIP, needSite, "xray"); err != nil {
|
||
nlog.Core().Warn("geo database unavailable", "error", err)
|
||
}
|
||
os.Setenv("XRAY_LOCATION_ASSET", dir)
|
||
}
|
||
|
||
// marshalConfig builds the xray JSON config and returns the raw bytes.
|
||
func marshalConfig(cfg config.KernelConfig, nc *model.NodeSpec, users []model.UserSpec, tls kernel.TLSCert) ([]byte, error) {
|
||
cfgMap := buildConfig(cfg, nc, users, tls)
|
||
data, err := json.MarshalIndent(cfgMap, "", " ")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("marshal config: %w", err)
|
||
}
|
||
nlog.Core().Debug("xray config generated", "len", len(data))
|
||
return data, nil
|
||
}
|
||
|
||
// startWithTimeout runs instance.Start() with a bounded deadline.
|
||
func startWithTimeout(inst *xrayCore.Instance, timeout time.Duration) error {
|
||
done := make(chan error, 1)
|
||
go func() { done <- inst.Start() }()
|
||
|
||
select {
|
||
case err := <-done:
|
||
if err != nil {
|
||
return fmt.Errorf("start xray: %w", err)
|
||
}
|
||
return nil
|
||
case <-time.After(timeout):
|
||
go inst.Close()
|
||
return fmt.Errorf("start xray: timeout after %v", timeout)
|
||
}
|
||
}
|
||
|
||
// closeOld shuts down a previously running instance and its dispatcher.
|
||
// Recycling happens in a background goroutine to prevent the main thread
|
||
// from blocking on slow connection draining, enabling "hitless" reload.
|
||
func closeOld(inst *xrayCore.Instance, ld *LimitDispatcher) {
|
||
if inst == nil {
|
||
return
|
||
}
|
||
go func() {
|
||
// 1. Drain connections gracefully (best effort, e.g. 5 minutes)
|
||
// We use a much longer timeout here than the default Stop() because
|
||
// it's running in background and doesn't block new user connections.
|
||
if ld != nil {
|
||
drainConns(ld, 5*time.Minute)
|
||
}
|
||
// 2. Hard close
|
||
inst.Close()
|
||
if ld != nil {
|
||
ld.ResetConns()
|
||
}
|
||
nlog.Core().Debug("xray: old instance recycled")
|
||
}()
|
||
}
|
||
|
||
// drainConns waits up to timeout for the dispatcher's active connections to
|
||
// reach zero. Used only during graceful Stop, not during hot-reload.
|
||
func drainConns(ld *LimitDispatcher, timeout time.Duration) {
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
if ld.connCount.Load() == 0 {
|
||
return
|
||
}
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
}
|
||
|
||
// aggregateStats is a fallback path that reads xray's built-in stats counters
|
||
// when LimitDispatcher is not available. Returns per-user cumulative traffic.
|
||
// Must be called with x.mu held.
|
||
func (x *Xray) aggregateStats() (map[int][2]int64, error) {
|
||
sm := x.instance.GetFeature(stats.ManagerType())
|
||
if sm == nil {
|
||
return nil, nil
|
||
}
|
||
mgr, ok := sm.(stats.Manager)
|
||
if !ok {
|
||
return nil, nil
|
||
}
|
||
|
||
traffic := make(map[int][2]int64)
|
||
for _, u := range x.users {
|
||
email := userEmail(u.ID)
|
||
|
||
var dUp, dDown int64
|
||
if c := mgr.GetCounter(fmt.Sprintf("user>>>%s>>>traffic>>>uplink", email)); c != nil {
|
||
dUp = c.Set(0)
|
||
}
|
||
if c := mgr.GetCounter(fmt.Sprintf("user>>>%s>>>traffic>>>downlink", email)); c != nil {
|
||
dDown = c.Set(0)
|
||
}
|
||
|
||
if dUp > 0 || dDown > 0 {
|
||
cum := x.cumTraffic[u.ID]
|
||
cum[0] += dUp
|
||
cum[1] += dDown
|
||
x.cumTraffic[u.ID] = cum
|
||
}
|
||
|
||
if cum := x.cumTraffic[u.ID]; cum[0] > 0 || cum[1] > 0 {
|
||
traffic[u.ID] = cum
|
||
}
|
||
}
|
||
return traffic, nil
|
||
}
|
||
|
||
// updateBandwidthLimits configures the patched xray-core bandwidth feature
|
||
// with per-user speed limits in bytes per second.
|
||
func (x *Xray) updateBandwidthLimits(users []model.UserSpec) {
|
||
x.mu.Lock()
|
||
inst := x.instance
|
||
fn := x.speedLimitFunc
|
||
currentUsers := x.users
|
||
x.mu.Unlock()
|
||
if inst == nil {
|
||
return
|
||
}
|
||
feat := inst.GetFeature(featurebandwidth.ManagerType())
|
||
if feat == nil {
|
||
return
|
||
}
|
||
bm, ok := feat.(featurebandwidth.Manager)
|
||
if !ok {
|
||
return
|
||
}
|
||
bm.Reset()
|
||
if users == nil {
|
||
users = currentUsers
|
||
}
|
||
for _, u := range users {
|
||
email := userEmail(u.ID)
|
||
if fn != nil {
|
||
bm.SetUserLimiter(email, fn(u.UUID))
|
||
continue
|
||
}
|
||
bps := int64(u.SpeedLimit) * 1_000_000 / 8
|
||
bm.SetUserLimit(email, bps)
|
||
}
|
||
}
|
||
|
||
// updateDispatcherLimits configures the LimitDispatcher with per-user
|
||
// device-limit admission metadata only. Speed limits are enforced by the
|
||
// patched xray-core bandwidth feature.
|
||
func (x *Xray) updateDispatcherLimits(users []model.UserSpec) {
|
||
x.mu.Lock()
|
||
ld := x.limitDispatcher
|
||
x.mu.Unlock()
|
||
if ld == nil {
|
||
return
|
||
}
|
||
|
||
emailToUID := make(map[string]int, len(users)*2)
|
||
deviceLimits := make(map[string]int)
|
||
|
||
for _, u := range users {
|
||
email := userEmail(u.ID)
|
||
emailToUID[email] = u.ID
|
||
emailToUID[u.UUID] = u.ID
|
||
if u.DeviceLimit > 0 {
|
||
deviceLimits[email] = u.DeviceLimit
|
||
deviceLimits[u.UUID] = u.DeviceLimit
|
||
}
|
||
}
|
||
|
||
ld.UpdateLimits(emailToUID, deviceLimits, nil)
|
||
}
|
||
|
||
// xrayCreationMu serialises xrayCore.New() + globalLimitDispatcher capture
|
||
// so that concurrent Xray instances in multi-node mode each capture their
|
||
// own LimitDispatcher.
|
||
var xrayCreationMu sync.Mutex
|