格式化
This commit is contained in:
+20
-50
@@ -8,10 +8,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
|
||||||
|
|
||||||
"mmw-agent/internal/agent"
|
"mmw-agent/internal/agent"
|
||||||
"mmw-agent/internal/config"
|
"mmw-agent/internal/config"
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
"mmw-agent/internal/handler"
|
"mmw-agent/internal/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,19 +20,19 @@ func main() {
|
|||||||
configPathShort := flag.String("c", "", "Path to config file (shorthand)")
|
configPathShort := flag.String("c", "", "Path to config file (shorthand)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// -c takes effect if -config is not set
|
// 仅在 -config 未设置时使用 -c
|
||||||
cfgFile := *configPath
|
cfgFile := *configPath
|
||||||
if cfgFile == "" {
|
if cfgFile == "" {
|
||||||
cfgFile = *configPathShort
|
cfgFile = *configPathShort
|
||||||
}
|
}
|
||||||
// Default to config.yaml in working directory
|
// 默认读取工作目录下的 config.yaml
|
||||||
if cfgFile == "" {
|
if cfgFile == "" {
|
||||||
if _, err := os.Stat("config.yaml"); err == nil {
|
if _, err := os.Stat("config.yaml"); err == nil {
|
||||||
cfgFile = "config.yaml"
|
cfgFile = "config.yaml"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// 加载配置
|
||||||
var cfg *config.Config
|
var cfg *config.Config
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load config: %v", err)
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
// Merge environment variables (env takes precedence)
|
// 合并环境变量(环境变量优先)
|
||||||
cfg.Merge(config.FromEnv())
|
cfg.Merge(config.FromEnv())
|
||||||
} else {
|
} else {
|
||||||
cfg = config.FromEnv()
|
cfg = config.FromEnv()
|
||||||
@@ -56,72 +56,42 @@ func main() {
|
|||||||
log.Printf("[Main] Listen port: %s", cfg.ListenPort)
|
log.Printf("[Main] Listen port: %s", cfg.ListenPort)
|
||||||
log.Printf("[Main] Xray servers: %d configured", len(cfg.XrayServers))
|
log.Printf("[Main] Xray servers: %d configured", len(cfg.XrayServers))
|
||||||
|
|
||||||
// Create agent client
|
// 创建 agent 客户端
|
||||||
agentClient := agent.NewClient(cfg)
|
agentClient := agent.NewClient(cfg)
|
||||||
|
|
||||||
// Create handlers
|
// 创建处理器
|
||||||
apiHandler := handler.NewAPIHandler(agentClient, cfg.Token)
|
apiHandler := handler.NewAPIHandler(agentClient, cfg.Token)
|
||||||
manageHandler := handler.NewManageHandler(cfg.Token)
|
manageHandler := handler.NewManageHandler(cfg.Token)
|
||||||
|
|
||||||
// Setup HTTP routes
|
// 注册 HTTP 路由
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
handler.RegisterChildRoutes(mux, apiHandler, manageHandler)
|
||||||
|
|
||||||
// Pull mode API
|
// 健康检查
|
||||||
mux.HandleFunc("/api/child/traffic", apiHandler.ServeHTTP)
|
mux.HandleFunc(constants.PathHealth, func(w http.ResponseWriter, r *http.Request) {
|
||||||
mux.HandleFunc("/api/child/speed", apiHandler.ServeSpeedHTTP)
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
|
|
||||||
// Management API
|
|
||||||
mux.HandleFunc("/api/child/services/status", manageHandler.HandleServicesStatus)
|
|
||||||
mux.HandleFunc("/api/child/services/control", manageHandler.HandleServiceControl)
|
|
||||||
mux.HandleFunc("/api/child/xray/install", manageHandler.HandleXrayInstall)
|
|
||||||
mux.HandleFunc("/api/child/xray/remove", manageHandler.HandleXrayRemove)
|
|
||||||
mux.HandleFunc("/api/child/xray/config", manageHandler.HandleXrayConfig)
|
|
||||||
mux.HandleFunc("/api/child/xray/system-config", manageHandler.HandleXraySystemConfig)
|
|
||||||
mux.HandleFunc("/api/child/xray/config-files", manageHandler.HandleXrayConfigFiles)
|
|
||||||
mux.HandleFunc("/api/child/nginx/install", manageHandler.HandleNginxInstall)
|
|
||||||
mux.HandleFunc("/api/child/nginx/remove", manageHandler.HandleNginxRemove)
|
|
||||||
mux.HandleFunc("/api/child/nginx/config", manageHandler.HandleNginxConfig)
|
|
||||||
mux.HandleFunc("/api/child/nginx/config-files", manageHandler.HandleNginxConfigFiles)
|
|
||||||
mux.HandleFunc("/api/child/system/info", manageHandler.HandleSystemInfo)
|
|
||||||
mux.HandleFunc("/api/child/inbounds", manageHandler.HandleInbounds)
|
|
||||||
mux.HandleFunc("/api/child/outbounds", manageHandler.HandleOutbounds)
|
|
||||||
mux.HandleFunc("/api/child/routing", manageHandler.HandleRouting)
|
|
||||||
mux.HandleFunc("/api/child/scan", manageHandler.HandleScan)
|
|
||||||
mux.HandleFunc("/api/child/cert/deploy", manageHandler.HandleCertDeploy)
|
|
||||||
mux.HandleFunc("/api/child/nginx/setup-ssl", manageHandler.HandleNginxSetupSSL)
|
|
||||||
mux.HandleFunc("/api/child/domains/latency", manageHandler.HandleDomainLatencyProbe)
|
|
||||||
|
|
||||||
// SSE streaming install/remove
|
|
||||||
mux.HandleFunc("/api/child/xray/install-stream", manageHandler.HandleXrayInstallStream)
|
|
||||||
mux.HandleFunc("/api/child/xray/remove-stream", manageHandler.HandleXrayRemoveStream)
|
|
||||||
mux.HandleFunc("/api/child/nginx/install-stream", manageHandler.HandleNginxInstallStream)
|
|
||||||
mux.HandleFunc("/api/child/nginx/remove-stream", manageHandler.HandleNginxRemoveStream)
|
|
||||||
|
|
||||||
// Health check
|
|
||||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte(`{"status":"ok","mode":"` + string(agentClient.GetCurrentMode()) + `"}`))
|
w.Write([]byte(`{"status":"ok","mode":"` + string(agentClient.GetCurrentMode()) + `"}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create HTTP server (no WriteTimeout — SSE streaming needs long-lived connections)
|
// 创建 HTTP 服务(不设置 WriteTimeout,避免影响 SSE 长连接)
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":" + cfg.ListenPort,
|
Addr: ":" + cfg.ListenPort,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: constants.DefaultReadTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup graceful shutdown
|
// 配置优雅退出
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
// Start agent client
|
// 启动 agent 客户端
|
||||||
agentClient.Start(ctx)
|
agentClient.Start(ctx)
|
||||||
|
|
||||||
// Start HTTP server
|
// 启动 HTTP 服务
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[Main] HTTP server listening on :%s", cfg.ListenPort)
|
log.Printf("[Main] HTTP server listening on :%s", cfg.ListenPort)
|
||||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
@@ -129,15 +99,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait for shutdown signal
|
// 等待退出信号
|
||||||
sig := <-sigCh
|
sig := <-sigCh
|
||||||
log.Printf("[Main] Received signal %v, shutting down...", sig)
|
log.Printf("[Main] Received signal %v, shutting down...", sig)
|
||||||
|
|
||||||
// Graceful shutdown
|
// 优雅退出
|
||||||
cancel()
|
cancel()
|
||||||
agentClient.Stop()
|
agentClient.Stop()
|
||||||
|
|
||||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), constants.DefaultShutdownTimeout)
|
||||||
defer shutdownCancel()
|
defer shutdownCancel()
|
||||||
|
|
||||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
|||||||
+183
-110
@@ -21,11 +21,12 @@ import (
|
|||||||
|
|
||||||
"mmw-agent/internal/collector"
|
"mmw-agent/internal/collector"
|
||||||
"mmw-agent/internal/config"
|
"mmw-agent/internal/config"
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionMode represents the current connection mode
|
// ConnectionMode 表示当前连接模式。
|
||||||
type ConnectionMode string
|
type ConnectionMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -35,7 +36,7 @@ const (
|
|||||||
ModeAuto ConnectionMode = "auto"
|
ModeAuto ConnectionMode = "auto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents an agent client that connects to a master server
|
// Client 表示连接主控端的 agent 客户端。
|
||||||
type Client struct {
|
type Client struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
collector *collector.Collector
|
collector *collector.Collector
|
||||||
@@ -47,20 +48,20 @@ type Client struct {
|
|||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
// Connection state
|
// 连接状态
|
||||||
currentMode ConnectionMode
|
currentMode ConnectionMode
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
httpAvailable bool
|
httpAvailable bool
|
||||||
modeMu sync.RWMutex
|
modeMu sync.RWMutex
|
||||||
|
|
||||||
// Speed calculation (from system network interface)
|
// 速率计算(基于系统网卡统计)
|
||||||
lastRxBytes int64
|
lastRxBytes int64
|
||||||
lastTxBytes int64
|
lastTxBytes int64
|
||||||
lastSampleTime time.Time
|
lastSampleTime time.Time
|
||||||
speedMu sync.Mutex
|
speedMu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new agent client
|
// 创建 agent 客户端。
|
||||||
func NewClient(cfg *config.Config) *Client {
|
func NewClient(cfg *config.Config) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -68,20 +69,20 @@ func NewClient(cfg *config.Config) *Client {
|
|||||||
xrayServers: cfg.XrayServers,
|
xrayServers: cfg.XrayServers,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: constants.DefaultHTTPClientTimeout,
|
||||||
},
|
},
|
||||||
currentMode: ModePull, // Default to pull mode
|
currentMode: ModePull, // 默认使用拉取模式
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wsHeaders returns HTTP headers for WebSocket handshake
|
// 生成 WebSocket 握手请求头。
|
||||||
func (c *Client) wsHeaders() http.Header {
|
func (c *Client) wsHeaders() http.Header {
|
||||||
h := http.Header{}
|
h := http.Header{}
|
||||||
h.Set("User-Agent", config.AgentUserAgent)
|
h.Set(constants.HeaderUserAgent, constants.AgentUserAgent)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequest creates an HTTP request with standard headers (Content-Type, Authorization, User-Agent)
|
// 创建带标准请求头的 HTTP 请求。
|
||||||
func (c *Client) newRequest(ctx context.Context, method, urlStr string, body []byte) (*http.Request, error) {
|
func (c *Client) newRequest(ctx context.Context, method, urlStr string, body []byte) (*http.Request, error) {
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
var err error
|
var err error
|
||||||
@@ -93,13 +94,13 @@ func (c *Client) newRequest(ctx context.Context, method, urlStr string, body []b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
req.Header.Set("Authorization", "Bearer "+c.config.Token)
|
req.Header.Set(constants.HeaderAuthorization, constants.BearerPrefix+c.config.Token)
|
||||||
req.Header.Set("User-Agent", config.AgentUserAgent)
|
req.Header.Set(constants.HeaderUserAgent, constants.AgentUserAgent)
|
||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the agent client with automatic mode selection
|
// 按配置启动客户端。
|
||||||
func (c *Client) Start(ctx context.Context) {
|
func (c *Client) Start(ctx context.Context) {
|
||||||
log.Printf("[Agent] Starting in %s mode", c.config.ConnectionMode)
|
log.Printf("[Agent] Starting in %s mode", c.config.ConnectionMode)
|
||||||
|
|
||||||
@@ -117,7 +118,7 @@ func (c *Client) Start(ctx context.Context) {
|
|||||||
case ModePull:
|
case ModePull:
|
||||||
c.setCurrentMode(ModePull)
|
c.setCurrentMode(ModePull)
|
||||||
log.Printf("[Agent] Pull mode enabled - API will be served at /api/child/traffic and /api/child/speed")
|
log.Printf("[Agent] Pull mode enabled - API will be served at /api/child/traffic and /api/child/speed")
|
||||||
// Report agent info immediately via HTTP heartbeat
|
// 启动后先通过 HTTP 上报一次心跳信息
|
||||||
if err := c.sendHeartbeatHTTP(ctx); err != nil {
|
if err := c.sendHeartbeatHTTP(ctx); err != nil {
|
||||||
log.Printf("[Agent] Failed to send initial heartbeat in pull mode: %v", err)
|
log.Printf("[Agent] Failed to send initial heartbeat in pull mode: %v", err)
|
||||||
}
|
}
|
||||||
@@ -130,7 +131,7 @@ func (c *Client) Start(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the agent client
|
// 停止客户端。
|
||||||
func (c *Client) Stop() {
|
func (c *Client) Stop() {
|
||||||
close(c.stopCh)
|
close(c.stopCh)
|
||||||
c.wg.Wait()
|
c.wg.Wait()
|
||||||
@@ -144,33 +145,33 @@ func (c *Client) Stop() {
|
|||||||
log.Printf("[Agent] Stopped")
|
log.Printf("[Agent] Stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConnected returns whether the WebSocket is connected
|
// 返回 WebSocket 连接状态。
|
||||||
func (c *Client) IsConnected() bool {
|
func (c *Client) IsConnected() bool {
|
||||||
c.wsMu.Lock()
|
c.wsMu.Lock()
|
||||||
defer c.wsMu.Unlock()
|
defer c.wsMu.Unlock()
|
||||||
return c.connected
|
return c.connected
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentMode returns the current connection mode
|
// 返回当前连接模式。
|
||||||
func (c *Client) GetCurrentMode() ConnectionMode {
|
func (c *Client) GetCurrentMode() ConnectionMode {
|
||||||
c.modeMu.RLock()
|
c.modeMu.RLock()
|
||||||
defer c.modeMu.RUnlock()
|
defer c.modeMu.RUnlock()
|
||||||
return c.currentMode
|
return c.currentMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCurrentMode sets the current connection mode
|
// 设置当前连接模式。
|
||||||
func (c *Client) setCurrentMode(mode ConnectionMode) {
|
func (c *Client) setCurrentMode(mode ConnectionMode) {
|
||||||
c.modeMu.Lock()
|
c.modeMu.Lock()
|
||||||
defer c.modeMu.Unlock()
|
defer c.modeMu.Unlock()
|
||||||
c.currentMode = mode
|
c.currentMode = mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// runWebSocket manages the WebSocket connection lifecycle with fallback to auto mode
|
// 维护 WebSocket 连接,并在失败时回退自动模式。
|
||||||
func (c *Client) runWebSocket(ctx context.Context) {
|
func (c *Client) runWebSocket(ctx context.Context) {
|
||||||
defer c.wg.Done()
|
defer c.wg.Done()
|
||||||
|
|
||||||
maxConsecutiveFailures := 5
|
maxConsecutiveFailures := constants.WebSocketMaxConsecutiveFailures
|
||||||
maxAuthFailures := 10
|
maxAuthFailures := constants.WebSocketMaxAuthFailures
|
||||||
consecutiveFailures := 0
|
consecutiveFailures := 0
|
||||||
authFailures := 0
|
authFailures := 0
|
||||||
|
|
||||||
@@ -190,22 +191,22 @@ func (c *Client) runWebSocket(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is an authentication error
|
// 判断是否为鉴权错误
|
||||||
if authErr, ok := err.(*AuthError); ok {
|
if authErr, ok := err.(*AuthError); ok {
|
||||||
authFailures++
|
authFailures++
|
||||||
if authErr.IsTokenInvalid() {
|
if authErr.IsTokenInvalid() {
|
||||||
log.Printf("[Agent] Authentication failed (invalid token): %v", err)
|
log.Printf("[Agent] Authentication failed (invalid token): %v", err)
|
||||||
if authFailures >= maxAuthFailures {
|
if authFailures >= maxAuthFailures {
|
||||||
log.Printf("[Agent] Too many auth failures (%d), entering sleep mode (30 min backoff)", authFailures)
|
log.Printf("[Agent] Too many auth failures (%d), entering sleep mode (30 min backoff)", authFailures)
|
||||||
c.waitWithTrafficReport(ctx, 30*time.Minute)
|
c.waitWithTrafficReport(ctx, constants.AuthFailureSleepBackoff)
|
||||||
authFailures = 0
|
authFailures = 0
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Use longer backoff for auth errors
|
// 鉴权错误使用更长退避时间
|
||||||
backoff := time.Duration(authFailures) * 30 * time.Second
|
backoff := time.Duration(authFailures) * constants.AuthFailureBackoffStep
|
||||||
if backoff > 10*time.Minute {
|
if backoff > constants.AuthFailureMaxBackoff {
|
||||||
backoff = 10 * time.Minute
|
backoff = constants.AuthFailureMaxBackoff
|
||||||
}
|
}
|
||||||
log.Printf("[Agent] Auth error, reconnecting in %v...", backoff)
|
log.Printf("[Agent] Auth error, reconnecting in %v...", backoff)
|
||||||
c.waitWithTrafficReport(ctx, backoff)
|
c.waitWithTrafficReport(ctx, backoff)
|
||||||
@@ -233,21 +234,21 @@ func (c *Client) runWebSocket(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateBackoff calculates the reconnection backoff duration with exponential increase
|
// 计算重连退避时长。
|
||||||
func (c *Client) calculateBackoff() time.Duration {
|
func (c *Client) calculateBackoff() time.Duration {
|
||||||
c.reconnects++
|
c.reconnects++
|
||||||
// Exponential backoff: 5s, 10s, 20s, 40s, 80s, 160s, 300s(cap)
|
// 指数退避: 5s, 10s, 20s, 40s, 80s, 160s, 300s(上限)
|
||||||
backoff := 5 * time.Second
|
backoff := constants.ReconnectBaseBackoff
|
||||||
for i := 1; i < c.reconnects && backoff < 5*time.Minute; i++ {
|
for i := 1; i < c.reconnects && backoff < constants.ReconnectMaxBackoff; i++ {
|
||||||
backoff *= 2
|
backoff *= 2
|
||||||
}
|
}
|
||||||
if backoff > 5*time.Minute {
|
if backoff > constants.ReconnectMaxBackoff {
|
||||||
backoff = 5 * time.Minute
|
backoff = constants.ReconnectMaxBackoff
|
||||||
}
|
}
|
||||||
return backoff
|
return backoff
|
||||||
}
|
}
|
||||||
|
|
||||||
// connectAndRun establishes and maintains a WebSocket connection
|
// 建立并维持 WebSocket 连接。
|
||||||
func (c *Client) connectAndRun(ctx context.Context) error {
|
func (c *Client) connectAndRun(ctx context.Context) error {
|
||||||
masterURL := c.config.MasterURL
|
masterURL := c.config.MasterURL
|
||||||
u, err := url.Parse(masterURL)
|
u, err := url.Parse(masterURL)
|
||||||
@@ -262,12 +263,12 @@ func (c *Client) connectAndRun(ctx context.Context) error {
|
|||||||
u.Scheme = "wss"
|
u.Scheme = "wss"
|
||||||
}
|
}
|
||||||
|
|
||||||
u.Path = "/api/remote/ws"
|
u.Path = constants.PathRemoteWebSocket
|
||||||
|
|
||||||
log.Printf("[Agent] Connecting to %s", u.String())
|
log.Printf("[Agent] Connecting to %s", u.String())
|
||||||
|
|
||||||
dialer := websocket.Dialer{
|
dialer := websocket.Dialer{
|
||||||
HandshakeTimeout: 10 * time.Second,
|
HandshakeTimeout: constants.WebSocketHandshakeTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, _, err := dialer.DialContext(ctx, u.String(), c.wsHeaders())
|
conn, _, err := dialer.DialContext(ctx, u.String(), c.wsHeaders())
|
||||||
@@ -298,18 +299,18 @@ func (c *Client) connectAndRun(ctx context.Context) error {
|
|||||||
|
|
||||||
log.Printf("[Agent] Connected and authenticated")
|
log.Printf("[Agent] Connected and authenticated")
|
||||||
|
|
||||||
// Report agent info (listen_port) immediately after connection
|
// 连接成功后立即上报 agent 信息(listen_port)
|
||||||
if err := c.sendHeartbeat(conn); err != nil {
|
if err := c.sendHeartbeat(conn); err != nil {
|
||||||
log.Printf("[Agent] Failed to send initial heartbeat: %v", err)
|
log.Printf("[Agent] Failed to send initial heartbeat: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send scan result to master for auto-sync
|
// 异步上报扫描结果,供主控端自动同步
|
||||||
go c.sendScanResult(conn)
|
go c.sendScanResult(conn)
|
||||||
|
|
||||||
return c.runMessageLoop(ctx, conn)
|
return c.runMessageLoop(ctx, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate sends the authentication message
|
// 发送鉴权消息。
|
||||||
func (c *Client) authenticate(conn *websocket.Conn) error {
|
func (c *Client) authenticate(conn *websocket.Conn) error {
|
||||||
authPayload, _ := json.Marshal(map[string]string{
|
authPayload, _ := json.Marshal(map[string]string{
|
||||||
"token": c.config.Token,
|
"token": c.config.Token,
|
||||||
@@ -324,7 +325,7 @@ func (c *Client) authenticate(conn *websocket.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
conn.SetReadDeadline(time.Now().Add(constants.WebSocketReadDeadline))
|
||||||
_, message, err := conn.ReadMessage()
|
_, message, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -349,11 +350,11 @@ func (c *Client) authenticate(conn *websocket.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runMessageLoop handles sending traffic data, speed data, and heartbeats
|
// 处理流量、速率和心跳上报。
|
||||||
func (c *Client) runMessageLoop(ctx context.Context, conn *websocket.Conn) error {
|
func (c *Client) runMessageLoop(ctx context.Context, conn *websocket.Conn) error {
|
||||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||||
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
||||||
heartbeatTicker := time.NewTicker(30 * time.Second)
|
heartbeatTicker := time.NewTicker(constants.WebSocketHeartbeatInterval)
|
||||||
defer trafficTicker.Stop()
|
defer trafficTicker.Stop()
|
||||||
defer speedTicker.Stop()
|
defer speedTicker.Stop()
|
||||||
defer heartbeatTicker.Stop()
|
defer heartbeatTicker.Stop()
|
||||||
@@ -362,13 +363,13 @@ func (c *Client) runMessageLoop(ctx context.Context, conn *websocket.Conn) error
|
|||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
|
conn.SetReadDeadline(time.Now().Add(constants.WebSocketIdleDeadline))
|
||||||
_, message, err := conn.ReadMessage()
|
_, message, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Send message to processing channel
|
// 投递到消息处理通道
|
||||||
select {
|
select {
|
||||||
case msgCh <- message:
|
case msgCh <- message:
|
||||||
default:
|
default:
|
||||||
@@ -406,7 +407,7 @@ func (c *Client) runMessageLoop(ctx context.Context, conn *websocket.Conn) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendTrafficData collects and sends traffic data to the master
|
// 采集并发送流量数据。
|
||||||
func (c *Client) sendTrafficData(conn *websocket.Conn) error {
|
func (c *Client) sendTrafficData(conn *websocket.Conn) error {
|
||||||
stats, err := c.collectLocalMetrics()
|
stats, err := c.collectLocalMetrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -437,7 +438,7 @@ func (c *Client) sendTrafficData(conn *websocket.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendHeartbeat sends a heartbeat message
|
// 发送心跳消息。
|
||||||
func (c *Client) sendHeartbeat(conn *websocket.Conn) error {
|
func (c *Client) sendHeartbeat(conn *websocket.Conn) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
||||||
@@ -458,7 +459,7 @@ func (c *Client) sendHeartbeat(conn *websocket.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectLocalMetrics collects traffic metrics from local Xray servers
|
// 采集本机 Xray 流量指标。
|
||||||
func (c *Client) collectLocalMetrics() (*collector.XrayStats, error) {
|
func (c *Client) collectLocalMetrics() (*collector.XrayStats, error) {
|
||||||
stats := &collector.XrayStats{
|
stats := &collector.XrayStats{
|
||||||
Inbound: make(map[string]collector.TrafficData),
|
Inbound: make(map[string]collector.TrafficData),
|
||||||
@@ -487,23 +488,23 @@ func (c *Client) collectLocalMetrics() (*collector.XrayStats, error) {
|
|||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats returns the current traffic stats (for pull mode)
|
// 返回当前流量统计(拉取模式)。
|
||||||
func (c *Client) GetStats() (*collector.XrayStats, error) {
|
func (c *Client) GetStats() (*collector.XrayStats, error) {
|
||||||
return c.collectLocalMetrics()
|
return c.collectLocalMetrics()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSpeed returns the current speed data (for pull mode)
|
// 返回当前速率(拉取模式)。
|
||||||
func (c *Client) GetSpeed() (uploadSpeed, downloadSpeed int64) {
|
func (c *Client) GetSpeed() (uploadSpeed, downloadSpeed int64) {
|
||||||
return c.collectSpeed()
|
return c.collectSpeed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// runAutoMode implements the three-tier fallback: WebSocket -> HTTP -> Pull
|
// 使用三层回退:WebSocket -> HTTP -> Pull。
|
||||||
func (c *Client) runAutoMode(ctx context.Context) {
|
func (c *Client) runAutoMode(ctx context.Context) {
|
||||||
defer c.wg.Done()
|
defer c.wg.Done()
|
||||||
c.runAutoModeLoop(ctx)
|
c.runAutoModeLoop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runAutoModeLoop is the internal loop for auto mode fallback
|
// 是自动模式的内部循环。
|
||||||
func (c *Client) runAutoModeLoop(ctx context.Context) {
|
func (c *Client) runAutoModeLoop(ctx context.Context) {
|
||||||
autoRetries := 0
|
autoRetries := 0
|
||||||
for {
|
for {
|
||||||
@@ -548,14 +549,14 @@ func (c *Client) runAutoModeLoop(ctx context.Context) {
|
|||||||
log.Printf("[Agent] Falling back to pull mode - API available at /api/child/traffic and /api/child/speed")
|
log.Printf("[Agent] Falling back to pull mode - API available at /api/child/traffic and /api/child/speed")
|
||||||
c.sendHeartbeatHTTP(ctx)
|
c.sendHeartbeatHTTP(ctx)
|
||||||
|
|
||||||
// Exponential backoff for pull mode: 30s, 60s, 120s, 240s, 300s(cap)
|
// 拉取模式退避: 30s, 60s, 120s, 240s, 300s(上限)
|
||||||
autoRetries++
|
autoRetries++
|
||||||
pullDuration := 30 * time.Second
|
pullDuration := constants.AutoModePullFallbackBackoff
|
||||||
for i := 1; i < autoRetries && pullDuration < 5*time.Minute; i++ {
|
for i := 1; i < autoRetries && pullDuration < constants.ReconnectMaxBackoff; i++ {
|
||||||
pullDuration *= 2
|
pullDuration *= 2
|
||||||
}
|
}
|
||||||
if pullDuration > 5*time.Minute {
|
if pullDuration > constants.ReconnectMaxBackoff {
|
||||||
pullDuration = 5 * time.Minute
|
pullDuration = constants.ReconnectMaxBackoff
|
||||||
}
|
}
|
||||||
|
|
||||||
c.runPullModeWithTrafficReport(ctx, pullDuration)
|
c.runPullModeWithTrafficReport(ctx, pullDuration)
|
||||||
@@ -567,7 +568,7 @@ func (c *Client) runAutoModeLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryWebSocketOnce attempts a single WebSocket connection test
|
// 执行一次 WebSocket 可用性探测。
|
||||||
func (c *Client) tryWebSocketOnce(ctx context.Context) error {
|
func (c *Client) tryWebSocketOnce(ctx context.Context) error {
|
||||||
masterURL := c.config.MasterURL
|
masterURL := c.config.MasterURL
|
||||||
u, err := url.Parse(masterURL)
|
u, err := url.Parse(masterURL)
|
||||||
@@ -581,10 +582,10 @@ func (c *Client) tryWebSocketOnce(ctx context.Context) error {
|
|||||||
case "https":
|
case "https":
|
||||||
u.Scheme = "wss"
|
u.Scheme = "wss"
|
||||||
}
|
}
|
||||||
u.Path = "/api/remote/ws"
|
u.Path = constants.PathRemoteWebSocket
|
||||||
|
|
||||||
dialer := websocket.Dialer{
|
dialer := websocket.Dialer{
|
||||||
HandshakeTimeout: 10 * time.Second,
|
HandshakeTimeout: constants.WebSocketHandshakeTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, _, err := dialer.DialContext(ctx, u.String(), c.wsHeaders())
|
conn, _, err := dialer.DialContext(ctx, u.String(), c.wsHeaders())
|
||||||
@@ -595,13 +596,13 @@ func (c *Client) tryWebSocketOnce(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryHTTPOnce tests if HTTP push is available
|
// 探测 HTTP 推送是否可用。
|
||||||
func (c *Client) tryHTTPOnce(ctx context.Context) bool {
|
func (c *Client) tryHTTPOnce(ctx context.Context) bool {
|
||||||
u, err := url.Parse(c.config.MasterURL)
|
u, err := url.Parse(c.config.MasterURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
u.Path = "/api/remote/heartbeat"
|
u.Path = constants.PathRemoteHeartbeat
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodPost, u.String(), []byte("{}"))
|
req, err := c.newRequest(ctx, http.MethodPost, u.String(), []byte("{}"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -619,18 +620,18 @@ func (c *Client) tryHTTPOnce(ctx context.Context) bool {
|
|||||||
return c.httpAvailable
|
return c.httpAvailable
|
||||||
}
|
}
|
||||||
|
|
||||||
// runHTTPReporter runs the HTTP push reporter
|
// 运行 HTTP 推送上报器。
|
||||||
func (c *Client) runHTTPReporter(ctx context.Context) {
|
func (c *Client) runHTTPReporter(ctx context.Context) {
|
||||||
defer c.wg.Done()
|
defer c.wg.Done()
|
||||||
c.setCurrentMode(ModeHTTP)
|
c.setCurrentMode(ModeHTTP)
|
||||||
c.runHTTPReporterLoop(ctx)
|
c.runHTTPReporterLoop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// runHTTPReporterLoop runs the HTTP reporting loop
|
// 执行 HTTP 上报循环。
|
||||||
func (c *Client) runHTTPReporterLoop(ctx context.Context) {
|
func (c *Client) runHTTPReporterLoop(ctx context.Context) {
|
||||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||||
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
speedTicker := time.NewTicker(c.config.SpeedReportInterval)
|
||||||
heartbeatTicker := time.NewTicker(30 * time.Second)
|
heartbeatTicker := time.NewTicker(constants.WebSocketHeartbeatInterval)
|
||||||
defer trafficTicker.Stop()
|
defer trafficTicker.Stop()
|
||||||
defer speedTicker.Stop()
|
defer speedTicker.Stop()
|
||||||
defer heartbeatTicker.Stop()
|
defer heartbeatTicker.Stop()
|
||||||
@@ -676,7 +677,7 @@ func (c *Client) runHTTPReporterLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendTrafficHTTP sends traffic data via HTTP POST
|
// 通过 HTTP POST 发送流量数据。
|
||||||
func (c *Client) sendTrafficHTTP(ctx context.Context) error {
|
func (c *Client) sendTrafficHTTP(ctx context.Context) error {
|
||||||
stats, err := c.collectLocalMetrics()
|
stats, err := c.collectLocalMetrics()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -691,7 +692,7 @@ func (c *Client) sendTrafficHTTP(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.Path = "/api/remote/traffic"
|
u.Path = constants.PathRemoteTraffic
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -714,7 +715,7 @@ func (c *Client) sendTrafficHTTP(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendSpeedHTTP sends speed data via HTTP POST
|
// 通过 HTTP POST 发送速率数据。
|
||||||
func (c *Client) sendSpeedHTTP(ctx context.Context) error {
|
func (c *Client) sendSpeedHTTP(ctx context.Context) error {
|
||||||
uploadSpeed, downloadSpeed := c.collectSpeed()
|
uploadSpeed, downloadSpeed := c.collectSpeed()
|
||||||
|
|
||||||
@@ -727,7 +728,7 @@ func (c *Client) sendSpeedHTTP(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.Path = "/api/remote/speed"
|
u.Path = constants.PathRemoteSpeed
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -749,7 +750,7 @@ func (c *Client) sendSpeedHTTP(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendHeartbeatHTTP sends heartbeat via HTTP POST
|
// 通过 HTTP POST 发送心跳。
|
||||||
func (c *Client) sendHeartbeatHTTP(ctx context.Context) error {
|
func (c *Client) sendHeartbeatHTTP(ctx context.Context) error {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
listenPort, _ := strconv.Atoi(c.config.ListenPort)
|
||||||
@@ -762,7 +763,7 @@ func (c *Client) sendHeartbeatHTTP(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.Path = "/api/remote/heartbeat"
|
u.Path = constants.PathRemoteHeartbeat
|
||||||
|
|
||||||
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
req, err := c.newRequest(ctx, http.MethodPost, u.String(), payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -783,7 +784,7 @@ func (c *Client) sendHeartbeatHTTP(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// runPullModeWithTrafficReport runs pull mode while sending traffic data to keep server online
|
// 在拉取模式下持续上报流量,保持在线状态。
|
||||||
func (c *Client) runPullModeWithTrafficReport(ctx context.Context, duration time.Duration) {
|
func (c *Client) runPullModeWithTrafficReport(ctx context.Context, duration time.Duration) {
|
||||||
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
trafficTicker := time.NewTicker(c.config.TrafficReportInterval)
|
||||||
defer trafficTicker.Stop()
|
defer trafficTicker.Stop()
|
||||||
@@ -810,13 +811,13 @@ func (c *Client) runPullModeWithTrafficReport(ctx context.Context, duration time
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitWithTrafficReport waits for the specified duration while sending traffic data
|
// 在等待期间继续上报流量。
|
||||||
func (c *Client) waitWithTrafficReport(ctx context.Context, duration time.Duration) {
|
func (c *Client) waitWithTrafficReport(ctx context.Context, duration time.Duration) {
|
||||||
if duration <= 0 {
|
if duration <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if duration > 30*time.Second {
|
if duration > constants.PullModeTrafficReportThreshold {
|
||||||
if err := c.sendTrafficHTTP(ctx); err != nil {
|
if err := c.sendTrafficHTTP(ctx); err != nil {
|
||||||
log.Printf("[Agent] Traffic report during backoff failed: %v", err)
|
log.Printf("[Agent] Traffic report during backoff failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -843,7 +844,7 @@ func (c *Client) waitWithTrafficReport(ctx context.Context, duration time.Durati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendSpeedData sends speed data via WebSocket
|
// 通过 WebSocket 发送速率数据。
|
||||||
func (c *Client) sendSpeedData(conn *websocket.Conn) error {
|
func (c *Client) sendSpeedData(conn *websocket.Conn) error {
|
||||||
uploadSpeed, downloadSpeed := c.collectSpeed()
|
uploadSpeed, downloadSpeed := c.collectSpeed()
|
||||||
|
|
||||||
@@ -869,7 +870,7 @@ func (c *Client) sendSpeedData(conn *websocket.Conn) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectSpeed calculates the current upload and download speed from system network interface
|
// 基于系统网卡统计计算当前上下行速率。
|
||||||
func (c *Client) collectSpeed() (uploadSpeed, downloadSpeed int64) {
|
func (c *Client) collectSpeed() (uploadSpeed, downloadSpeed int64) {
|
||||||
c.speedMu.Lock()
|
c.speedMu.Lock()
|
||||||
defer c.speedMu.Unlock()
|
defer c.speedMu.Unlock()
|
||||||
@@ -900,7 +901,7 @@ func (c *Client) collectSpeed() (uploadSpeed, downloadSpeed int64) {
|
|||||||
return uploadSpeed, downloadSpeed
|
return uploadSpeed, downloadSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSystemNetworkStats reads network statistics from /proc/net/dev
|
// 从 /proc/net/dev 读取网卡统计。
|
||||||
func (c *Client) getSystemNetworkStats() (rxBytes, txBytes int64) {
|
func (c *Client) getSystemNetworkStats() (rxBytes, txBytes int64) {
|
||||||
data, err := os.ReadFile("/proc/net/dev")
|
data, err := os.ReadFile("/proc/net/dev")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -936,7 +937,7 @@ func (c *Client) getSystemNetworkStats() (rxBytes, txBytes int64) {
|
|||||||
return rxBytes, txBytes
|
return rxBytes, txBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthError represents an authentication error
|
// AuthError 表示鉴权失败错误。
|
||||||
type AuthError struct {
|
type AuthError struct {
|
||||||
Message string
|
Message string
|
||||||
Code string // "token_expired", "token_invalid", "server_error"
|
Code string // "token_expired", "token_invalid", "server_error"
|
||||||
@@ -946,12 +947,12 @@ func (e *AuthError) Error() string {
|
|||||||
return "authentication failed: " + e.Message
|
return "authentication failed: " + e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTokenInvalid returns true if the error indicates an invalid token
|
// 判断是否为 token 无效错误。
|
||||||
func (e *AuthError) IsTokenInvalid() bool {
|
func (e *AuthError) IsTokenInvalid() bool {
|
||||||
return e.Code == "token_invalid" || e.Message == "Invalid token"
|
return e.Code == "token_invalid" || e.Message == "Invalid token"
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket message types
|
// WebSocket 消息类型
|
||||||
const (
|
const (
|
||||||
WSMsgTypeCertDeploy = "cert_deploy"
|
WSMsgTypeCertDeploy = "cert_deploy"
|
||||||
WSMsgTypeTokenUpdate = "token_update"
|
WSMsgTypeTokenUpdate = "token_update"
|
||||||
@@ -960,7 +961,7 @@ const (
|
|||||||
WSMsgTypeDomainLatencyResult = "domain_latency_result"
|
WSMsgTypeDomainLatencyResult = "domain_latency_result"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WSCertDeployPayload represents a certificate deploy command from master
|
// WSCertDeployPayload 是主控端下发的证书部署指令。
|
||||||
type WSCertDeployPayload struct {
|
type WSCertDeployPayload struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
CertPEM string `json:"cert_pem"`
|
CertPEM string `json:"cert_pem"`
|
||||||
@@ -970,20 +971,20 @@ type WSCertDeployPayload struct {
|
|||||||
Reload string `json:"reload"`
|
Reload string `json:"reload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WSTokenUpdatePayload represents a token update from master
|
// WSTokenUpdatePayload 是主控端下发的 token 更新指令。
|
||||||
type WSTokenUpdatePayload struct {
|
type WSTokenUpdatePayload struct {
|
||||||
ServerToken string `json:"server_token"`
|
ServerToken string `json:"server_token"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WSDomainLatencyProbePayload is received from master
|
// WSDomainLatencyProbePayload 是主控端下发的域名延迟探测请求。
|
||||||
type WSDomainLatencyProbePayload struct {
|
type WSDomainLatencyProbePayload struct {
|
||||||
RequestID string `json:"request_id"`
|
RequestID string `json:"request_id"`
|
||||||
Domains []string `json:"domains"`
|
Domains []string `json:"domains"`
|
||||||
TimeoutMs int `json:"timeout_ms"`
|
TimeoutMs int `json:"timeout_ms"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleMessage processes incoming messages from master
|
// 处理主控端下发的消息。
|
||||||
func (c *Client) handleMessage(conn *websocket.Conn, message []byte) {
|
func (c *Client) handleMessage(conn *websocket.Conn, message []byte) {
|
||||||
var msg struct {
|
var msg struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -1018,11 +1019,11 @@ func (c *Client) handleMessage(conn *websocket.Conn, message []byte) {
|
|||||||
}
|
}
|
||||||
go c.handleDomainLatencyProbe(conn, payload)
|
go c.handleDomainLatencyProbe(conn, payload)
|
||||||
default:
|
default:
|
||||||
// Ignore unknown message types
|
// 忽略未知消息类型
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCertDeploy deploys a certificate received from master
|
// 处理主控端下发的证书部署。
|
||||||
func (c *Client) handleCertDeploy(payload WSCertDeployPayload) {
|
func (c *Client) handleCertDeploy(payload WSCertDeployPayload) {
|
||||||
log.Printf("[Agent] Received cert_deploy for domain: %s, target: %s", payload.Domain, payload.Reload)
|
log.Printf("[Agent] Received cert_deploy for domain: %s, target: %s", payload.Domain, payload.Reload)
|
||||||
|
|
||||||
@@ -1065,7 +1066,7 @@ func deployCert(certPEM, keyPEM, certPath, keyPath, reloadTarget string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reloadNginxCmd() error {
|
func reloadNginxCmd() error {
|
||||||
for _, bin := range []string{"/usr/local/nginx/sbin/nginx", "nginx"} {
|
for _, bin := range constants.NginxBinarySearchPaths {
|
||||||
if path, err := exec.LookPath(bin); err == nil {
|
if path, err := exec.LookPath(bin); err == nil {
|
||||||
return runCmd(path, "-s", "reload")
|
return runCmd(path, "-s", "reload")
|
||||||
}
|
}
|
||||||
@@ -1080,43 +1081,47 @@ func runCmd(name string, args ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTokenUpdate processes a token update from master
|
// 处理主控端下发的 token 更新。
|
||||||
func (c *Client) handleTokenUpdate(payload WSTokenUpdatePayload) {
|
func (c *Client) handleTokenUpdate(payload WSTokenUpdatePayload) {
|
||||||
log.Printf("[Agent] Received token update from master, new token expires at %s", payload.ExpiresAt.Format(time.RFC3339))
|
log.Printf("[Agent] Received token update from master, new token expires at %s", payload.ExpiresAt.Format(time.RFC3339))
|
||||||
|
|
||||||
// Update the token in memory
|
// 更新内存中的 token
|
||||||
c.config.Token = payload.ServerToken
|
c.config.Token = payload.ServerToken
|
||||||
|
|
||||||
log.Printf("[Agent] Token updated successfully in memory")
|
log.Printf("[Agent] Token updated successfully in memory")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDomainLatencyProbe probes domain latency locally and sends results back via WebSocket
|
// 在本机探测域名延迟并回传结果。
|
||||||
func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomainLatencyProbePayload) {
|
func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomainLatencyProbePayload) {
|
||||||
log.Printf("[Agent] Received domain_latency_probe: %d domains, timeout=%dms", len(payload.Domains), payload.TimeoutMs)
|
log.Printf("[Agent] Received domain_latency_probe: %d domains, timeout=%dms", len(payload.Domains), payload.TimeoutMs)
|
||||||
|
|
||||||
timeoutMs := payload.TimeoutMs
|
timeoutMs := payload.TimeoutMs
|
||||||
if timeoutMs <= 0 {
|
if timeoutMs <= 0 {
|
||||||
timeoutMs = 2000
|
timeoutMs = constants.DomainProbeDefaultTimeoutMS
|
||||||
}
|
}
|
||||||
if timeoutMs < 200 {
|
if timeoutMs < constants.DomainProbeMinTimeoutMS {
|
||||||
timeoutMs = 200
|
timeoutMs = constants.DomainProbeMinTimeoutMS
|
||||||
}
|
}
|
||||||
if timeoutMs > 10000 {
|
if timeoutMs > constants.DomainProbeMaxTimeoutMS {
|
||||||
timeoutMs = 10000
|
timeoutMs = constants.DomainProbeMaxTimeoutMS
|
||||||
}
|
}
|
||||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||||
|
|
||||||
type probeResult struct {
|
type probeResult struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
NginxSSLPort int `json:"nginx_ssl_port,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 读取本机 nginx 配置,构造 domain -> ssl 端口映射
|
||||||
|
nginxPortMap := readNginxSSLPorts(payload.Domains)
|
||||||
|
|
||||||
results := make([]probeResult, 0, len(payload.Domains))
|
results := make([]probeResult, 0, len(payload.Domains))
|
||||||
resultCh := make(chan probeResult, len(payload.Domains))
|
resultCh := make(chan probeResult, len(payload.Domains))
|
||||||
sem := make(chan struct{}, 16)
|
sem := make(chan struct{}, constants.DomainProbeConcurrency)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, domain := range payload.Domains {
|
for _, domain := range payload.Domains {
|
||||||
@@ -1145,7 +1150,7 @@ func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomain
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = tcpConn.Close()
|
_ = tcpConn.Close()
|
||||||
resultCh <- probeResult{Domain: host, Target: target, Success: true, LatencyMs: time.Since(start).Milliseconds()}
|
resultCh <- probeResult{Domain: host, Target: target, Success: true, LatencyMs: time.Since(start).Milliseconds(), NginxSSLPort: nginxPortMap[host]}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,7 +1160,7 @@ func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomain
|
|||||||
results = append(results, r)
|
results = append(results, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort: success first, then by latency
|
// 排序:成功优先,再按延迟升序
|
||||||
sort.Slice(results, func(i, j int) bool {
|
sort.Slice(results, func(i, j int) bool {
|
||||||
if results[i].Success != results[j].Success {
|
if results[i].Success != results[j].Success {
|
||||||
return results[i].Success
|
return results[i].Success
|
||||||
@@ -1201,9 +1206,80 @@ func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomain
|
|||||||
log.Printf("[Agent] Sent domain_latency_result: %d results", len(results))
|
log.Printf("[Agent] Sent domain_latency_result: %d results", len(results))
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendScanResult scans local xray status and sends results to master
|
// readNginxSSLPorts 读取 nginx 配置并返回 domain -> SSL 端口映射。
|
||||||
|
// 会在常见 nginx 配置目录下查找 servers/{domain}.conf。
|
||||||
|
func readNginxSSLPorts(domains []string) map[string]int {
|
||||||
|
result := make(map[string]int)
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
confDirs := constants.NginxSSLServerDirPaths
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
host := domain
|
||||||
|
if h, _, err := net.SplitHostPort(domain); err == nil && h != "" {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
for _, dir := range confDirs {
|
||||||
|
confPath := filepath.Join(dir, host+".conf")
|
||||||
|
data, err := os.ReadFile(confPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if port := extractSSLListenPort(string(data)); port > 0 {
|
||||||
|
result[host] = port
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 nginx 配置块中第一个 "listen <port> ssl" 端口。
|
||||||
|
func extractSSLListenPort(conf string) int {
|
||||||
|
// 匹配示例: listen 58443 ssl
|
||||||
|
for _, line := range strings.Split(conf, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, "listen") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 去掉 "listen" 前缀和结尾分号
|
||||||
|
rest := strings.TrimPrefix(line, "listen")
|
||||||
|
rest = strings.TrimRight(rest, ";")
|
||||||
|
rest = strings.TrimSpace(rest)
|
||||||
|
fields := strings.Fields(rest)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 判断字段中是否包含 "ssl"
|
||||||
|
hasSSL := false
|
||||||
|
for _, f := range fields[1:] {
|
||||||
|
if f == "ssl" {
|
||||||
|
hasSSL = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasSSL {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 第一个字段是端口(或 [::]:port)
|
||||||
|
portStr := fields[0]
|
||||||
|
// 兼容 [::]:port 形式
|
||||||
|
if idx := strings.LastIndex(portStr, ":"); idx >= 0 {
|
||||||
|
portStr = portStr[idx+1:]
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err == nil && port > 0 {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描本机 xray 状态并上报主控端。
|
||||||
func (c *Client) sendScanResult(conn *websocket.Conn) {
|
func (c *Client) sendScanResult(conn *websocket.Conn) {
|
||||||
// Check xray running status
|
// 检查 xray 运行状态
|
||||||
xrayRunning := false
|
xrayRunning := false
|
||||||
xrayVersion := ""
|
xrayVersion := ""
|
||||||
cmd := exec.Command("xray", "version")
|
cmd := exec.Command("xray", "version")
|
||||||
@@ -1214,12 +1290,9 @@ func (c *Client) sendScanResult(conn *websocket.Conn) {
|
|||||||
xrayRunning = true
|
xrayRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read inbounds from config
|
// 从配置读取入站列表
|
||||||
var inbounds []map[string]interface{}
|
var inbounds []map[string]interface{}
|
||||||
configPaths := []string{
|
configPaths := constants.DefaultXrayConfigPaths
|
||||||
"/usr/local/etc/xray/config.json",
|
|
||||||
"/etc/xray/config.json",
|
|
||||||
}
|
|
||||||
for _, cfgPath := range configPaths {
|
for _, cfgPath := range configPaths {
|
||||||
data, err := os.ReadFile(cfgPath)
|
data, err := os.ReadFile(cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,28 +8,29 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// XrayMetrics represents the metrics response from Xray's /debug/vars endpoint
|
// XrayMetrics 表示 Xray /debug/vars 的响应结构。
|
||||||
type XrayMetrics struct {
|
type XrayMetrics struct {
|
||||||
Stats *XrayStats `json:"stats,omitempty"`
|
Stats *XrayStats `json:"stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// XrayStats contains inbound, outbound, and user traffic stats
|
// XrayStats 包含入站、出站和用户维度的流量统计。
|
||||||
type XrayStats struct {
|
type XrayStats struct {
|
||||||
Inbound map[string]TrafficData `json:"inbound,omitempty"`
|
Inbound map[string]TrafficData `json:"inbound,omitempty"`
|
||||||
Outbound map[string]TrafficData `json:"outbound,omitempty"`
|
Outbound map[string]TrafficData `json:"outbound,omitempty"`
|
||||||
User map[string]TrafficData `json:"user,omitempty"`
|
User map[string]TrafficData `json:"user,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrafficData contains uplink and downlink traffic in bytes
|
// TrafficData 表示上下行流量(字节)。
|
||||||
type TrafficData struct {
|
type TrafficData struct {
|
||||||
Uplink int64 `json:"uplink"`
|
Uplink int64 `json:"uplink"`
|
||||||
Downlink int64 `json:"downlink"`
|
Downlink int64 `json:"downlink"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// XrayConfig represents the structure of xray config.json for reading metrics port
|
// XrayConfig 用于读取 xray config.json 中的 metrics 监听配置。
|
||||||
type XrayConfig struct {
|
type XrayConfig struct {
|
||||||
Log json.RawMessage `json:"log,omitempty"`
|
Log json.RawMessage `json:"log,omitempty"`
|
||||||
DNS json.RawMessage `json:"dns,omitempty"`
|
DNS json.RawMessage `json:"dns,omitempty"`
|
||||||
@@ -42,51 +43,51 @@ type XrayConfig struct {
|
|||||||
Metrics *MetricsConfig `json:"metrics,omitempty"`
|
Metrics *MetricsConfig `json:"metrics,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricsConfig represents the metrics section in xray config
|
// MetricsConfig 对应 xray 配置中的 metrics 段。
|
||||||
type MetricsConfig struct {
|
type MetricsConfig struct {
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
Listen string `json:"listen,omitempty"` // Format: "127.0.0.1:38889"
|
Listen string `json:"listen,omitempty"` // 格式示例: "127.0.0.1:38889"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collector collects traffic metrics from Xray servers
|
// Collector 负责采集 Xray 流量指标。
|
||||||
type Collector struct {
|
type Collector struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
defaultMetricsPort int
|
defaultMetricsPort int
|
||||||
defaultMetricsHost string
|
defaultMetricsHost string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCollector creates a new metrics collector
|
// 创建指标采集器。
|
||||||
func NewCollector() *Collector {
|
func NewCollector() *Collector {
|
||||||
return &Collector{
|
return &Collector{
|
||||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
httpClient: &http.Client{Timeout: constants.DefaultHTTPClientTimeout},
|
||||||
defaultMetricsPort: 38889,
|
defaultMetricsPort: constants.DefaultMetricsPort,
|
||||||
defaultMetricsHost: "127.0.0.1",
|
defaultMetricsHost: constants.DefaultMetricsHost,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetricsPortFromConfig reads the metrics port from xray config file
|
// 从 xray 配置中读取 metrics 监听地址和端口。
|
||||||
func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, error) {
|
func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, error) {
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
return "127.0.0.1", c.defaultMetricsPort, nil
|
return constants.DefaultMetricsHost, c.defaultMetricsPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "127.0.0.1", c.defaultMetricsPort, fmt.Errorf("read config file: %w", err)
|
return constants.DefaultMetricsHost, c.defaultMetricsPort, fmt.Errorf("read config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var config XrayConfig
|
var config XrayConfig
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
if err := json.Unmarshal(data, &config); err != nil {
|
||||||
return "127.0.0.1", c.defaultMetricsPort, fmt.Errorf("parse config file: %w", err)
|
return constants.DefaultMetricsHost, c.defaultMetricsPort, fmt.Errorf("parse config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Metrics == nil || config.Metrics.Listen == "" {
|
if config.Metrics == nil || config.Metrics.Listen == "" {
|
||||||
return "", 0, fmt.Errorf("metrics not configured in xray config")
|
return "", 0, fmt.Errorf("metrics not configured in xray config")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse listen address (format: "127.0.0.1:38889" or ":38889")
|
// 解析监听地址,支持 "127.0.0.1:38889" 或 ":38889"
|
||||||
listen := config.Metrics.Listen
|
listen := config.Metrics.Listen
|
||||||
host := "127.0.0.1"
|
host := constants.DefaultMetricsHost
|
||||||
var port int
|
var port int
|
||||||
|
|
||||||
if strings.Contains(listen, ":") {
|
if strings.Contains(listen, ":") {
|
||||||
@@ -102,7 +103,7 @@ func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, er
|
|||||||
port = p
|
port = p
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Try to parse as port only
|
// 兼容仅填写端口的写法
|
||||||
p, err := strconv.Atoi(listen)
|
p, err := strconv.Atoi(listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, fmt.Errorf("invalid metrics listen format: %s", listen)
|
return "", 0, fmt.Errorf("invalid metrics listen format: %s", listen)
|
||||||
@@ -117,7 +118,7 @@ func (c *Collector) GetMetricsPortFromConfig(configPath string) (string, int, er
|
|||||||
return host, port, nil
|
return host, port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchMetrics fetches metrics from Xray's /debug/vars endpoint
|
// 从 Xray 的 /debug/vars 拉取指标。
|
||||||
func (c *Collector) FetchMetrics(host string, port int) (*XrayMetrics, error) {
|
func (c *Collector) FetchMetrics(host string, port int) (*XrayMetrics, error) {
|
||||||
url := fmt.Sprintf("http://%s:%d/debug/vars", host, port)
|
url := fmt.Sprintf("http://%s:%d/debug/vars", host, port)
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ func (c *Collector) FetchMetrics(host string, port int) (*XrayMetrics, error) {
|
|||||||
return &metrics, nil
|
return &metrics, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeStats merges source stats into dest stats
|
// 将 source 的统计合并到 dest。
|
||||||
func MergeStats(dest, source *XrayStats) {
|
func MergeStats(dest, source *XrayStats) {
|
||||||
if source == nil {
|
if source == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
+24
-26
@@ -5,12 +5,14 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AgentUserAgent = "miaomiaowux/0.1"
|
const AgentUserAgent = constants.AgentUserAgent
|
||||||
|
|
||||||
// Config holds the agent configuration
|
// Config 保存 agent 的运行配置。
|
||||||
type Config struct {
|
type Config struct {
|
||||||
MasterURL string `yaml:"master_url"`
|
MasterURL string `yaml:"master_url"`
|
||||||
Token string `yaml:"token"`
|
Token string `yaml:"token"`
|
||||||
@@ -21,20 +23,16 @@ type Config struct {
|
|||||||
SpeedReportInterval time.Duration `yaml:"speed_report_interval"`
|
SpeedReportInterval time.Duration `yaml:"speed_report_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// XrayServer represents a local Xray server configuration
|
// XrayServer 表示本机 Xray 节点配置。
|
||||||
type XrayServer struct {
|
type XrayServer struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
ConfigPath string `yaml:"config_path"`
|
ConfigPath string `yaml:"config_path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultXrayConfigPaths are the default paths to search for Xray config
|
// DefaultXrayConfigPaths 是默认的 Xray 配置搜索路径。
|
||||||
var DefaultXrayConfigPaths = []string{
|
var DefaultXrayConfigPaths = append([]string(nil), constants.DefaultXrayConfigPaths...)
|
||||||
"/usr/local/etc/xray/config.json",
|
|
||||||
"/etc/xray/config.json",
|
|
||||||
"/opt/xray/config.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads configuration from a YAML file
|
// 从 YAML 文件加载配置。
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -46,13 +44,13 @@ func Load(path string) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply defaults
|
// 补齐默认值
|
||||||
config.applyDefaults()
|
config.applyDefaults()
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromEnv creates configuration from environment variables
|
// 从环境变量构造配置。
|
||||||
func FromEnv() *Config {
|
func FromEnv() *Config {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
MasterURL: os.Getenv("MMWX_MASTER_URL"),
|
MasterURL: os.Getenv("MMWX_MASTER_URL"),
|
||||||
@@ -61,14 +59,14 @@ func FromEnv() *Config {
|
|||||||
ListenPort: os.Getenv("MMWX_LISTEN_PORT"),
|
ListenPort: os.Getenv("MMWX_LISTEN_PORT"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Xray config path from env
|
// 读取 Xray 配置路径
|
||||||
if xrayConfig := os.Getenv("MMWX_XRAY_CONFIG"); xrayConfig != "" {
|
if xrayConfig := os.Getenv("MMWX_XRAY_CONFIG"); xrayConfig != "" {
|
||||||
config.XrayServers = []XrayServer{
|
config.XrayServers = []XrayServer{
|
||||||
{Name: "primary", ConfigPath: xrayConfig},
|
{Name: "primary", ConfigPath: xrayConfig},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse intervals
|
// 读取上报间隔
|
||||||
if interval := os.Getenv("MMWX_TRAFFIC_INTERVAL"); interval != "" {
|
if interval := os.Getenv("MMWX_TRAFFIC_INTERVAL"); interval != "" {
|
||||||
if d, err := time.ParseDuration(interval); err == nil {
|
if d, err := time.ParseDuration(interval); err == nil {
|
||||||
config.TrafficReportInterval = d
|
config.TrafficReportInterval = d
|
||||||
@@ -84,7 +82,7 @@ func FromEnv() *Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge merges environment config into file config (env takes precedence)
|
// 合并环境变量配置到文件配置(环境变量优先)。
|
||||||
func (c *Config) Merge(env *Config) {
|
func (c *Config) Merge(env *Config) {
|
||||||
if env.MasterURL != "" {
|
if env.MasterURL != "" {
|
||||||
c.MasterURL = env.MasterURL
|
c.MasterURL = env.MasterURL
|
||||||
@@ -109,28 +107,28 @@ func (c *Config) Merge(env *Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyDefaults sets default values for unset fields
|
// 为空字段填充默认值。
|
||||||
func (c *Config) applyDefaults() {
|
func (c *Config) applyDefaults() {
|
||||||
if c.ConnectionMode == "" {
|
if c.ConnectionMode == "" {
|
||||||
c.ConnectionMode = "auto"
|
c.ConnectionMode = constants.ConnectionModeAuto
|
||||||
}
|
}
|
||||||
if c.ListenPort == "" {
|
if c.ListenPort == "" {
|
||||||
c.ListenPort = "23889"
|
c.ListenPort = constants.DefaultListenPort
|
||||||
}
|
}
|
||||||
if c.TrafficReportInterval == 0 {
|
if c.TrafficReportInterval == 0 {
|
||||||
c.TrafficReportInterval = 1 * time.Minute
|
c.TrafficReportInterval = constants.DefaultTrafficReportInterval
|
||||||
}
|
}
|
||||||
if c.SpeedReportInterval == 0 {
|
if c.SpeedReportInterval == 0 {
|
||||||
c.SpeedReportInterval = 3 * time.Second
|
c.SpeedReportInterval = constants.DefaultSpeedReportInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-discover Xray servers if not configured
|
// 未显式配置时自动探测 Xray 配置
|
||||||
if len(c.XrayServers) == 0 {
|
if len(c.XrayServers) == 0 {
|
||||||
c.XrayServers = c.discoverXrayServers()
|
c.XrayServers = c.discoverXrayServers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// discoverXrayServers scans default paths for Xray config files
|
// 扫描默认路径中的 Xray 配置文件。
|
||||||
func (c *Config) discoverXrayServers() []XrayServer {
|
func (c *Config) discoverXrayServers() []XrayServer {
|
||||||
var servers []XrayServer
|
var servers []XrayServer
|
||||||
for i, path := range DefaultXrayConfigPaths {
|
for i, path := range DefaultXrayConfigPaths {
|
||||||
@@ -144,11 +142,11 @@ func (c *Config) discoverXrayServers() []XrayServer {
|
|||||||
return servers
|
return servers
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if the configuration is valid
|
// 校验配置是否合法。
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
// Token is required for non-pull modes
|
// 拉取模式之外通常需要 token
|
||||||
if c.ConnectionMode != "pull" && c.Token == "" {
|
if c.ConnectionMode != constants.ConnectionModePull && c.Token == "" {
|
||||||
// Allow empty token, will work in pull mode only
|
// 兼容空 token,实际仅拉取模式可正常工作
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
AgentUserAgent = "miaomiaowux/0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HeaderAuthorization = "Authorization"
|
||||||
|
HeaderContentType = "Content-Type"
|
||||||
|
HeaderMMRemoteToken = "MM-Remote-Token"
|
||||||
|
HeaderUserAgent = "User-Agent"
|
||||||
|
ContentTypeJSON = "application/json"
|
||||||
|
BearerPrefix = "Bearer "
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionModeAuto = "auto"
|
||||||
|
ConnectionModePull = "pull"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultListenPort = "23889"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultTrafficReportInterval = 1 * time.Minute
|
||||||
|
DefaultSpeedReportInterval = 3 * time.Second
|
||||||
|
DefaultHTTPClientTimeout = 10 * time.Second
|
||||||
|
DefaultReadTimeout = 30 * time.Second
|
||||||
|
DefaultShutdownTimeout = 10 * time.Second
|
||||||
|
DefaultRPCShortTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultMetricsHost = "127.0.0.1"
|
||||||
|
DefaultMetricsPort = 38889
|
||||||
|
DefaultMetricsListen = "127.0.0.1:38889"
|
||||||
|
LocalhostIP = "127.0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebSocketMaxConsecutiveFailures = 5
|
||||||
|
WebSocketMaxAuthFailures = 10
|
||||||
|
AuthFailureSleepBackoff = 30 * time.Minute
|
||||||
|
PullModeTrafficReportThreshold = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReconnectBaseBackoff = 5 * time.Second
|
||||||
|
ReconnectMaxBackoff = 5 * time.Minute
|
||||||
|
AuthFailureBackoffStep = 30 * time.Second
|
||||||
|
AuthFailureMaxBackoff = 10 * time.Minute
|
||||||
|
AutoModePullFallbackBackoff = 30 * time.Second
|
||||||
|
WebSocketHandshakeTimeout = 10 * time.Second
|
||||||
|
WebSocketReadDeadline = 10 * time.Second
|
||||||
|
WebSocketHeartbeatInterval = 30 * time.Second
|
||||||
|
WebSocketIdleDeadline = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DomainProbeDefaultTimeoutMS = 2000
|
||||||
|
DomainProbeMinTimeoutMS = 200
|
||||||
|
DomainProbeMaxTimeoutMS = 10000
|
||||||
|
DomainProbeMaxCount = 200
|
||||||
|
DomainProbeConcurrency = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
NginxPrimaryPrefixDir = "/usr/local/nginx"
|
||||||
|
|
||||||
|
DefaultXrayConfigPaths = []string{
|
||||||
|
"/usr/local/etc/xray/config.json",
|
||||||
|
"/etc/xray/config.json",
|
||||||
|
"/opt/xray/config.json",
|
||||||
|
}
|
||||||
|
XrayConfigDirPaths = []string{
|
||||||
|
"/usr/local/etc/xray",
|
||||||
|
"/etc/xray",
|
||||||
|
"/opt/xray",
|
||||||
|
}
|
||||||
|
DefaultNginxConfigPaths = []string{
|
||||||
|
"/etc/nginx/nginx.conf",
|
||||||
|
"/usr/local/nginx/conf/nginx.conf",
|
||||||
|
}
|
||||||
|
NginxConfigDirPaths = []string{
|
||||||
|
"/etc/nginx",
|
||||||
|
"/etc/nginx/sites-available",
|
||||||
|
"/etc/nginx/sites-enabled",
|
||||||
|
"/etc/nginx/conf.d",
|
||||||
|
"/usr/local/nginx/conf",
|
||||||
|
}
|
||||||
|
NginxSSLServerDirPaths = []string{
|
||||||
|
"/usr/local/nginx/servers",
|
||||||
|
"/usr/local/nginx/conf/servers",
|
||||||
|
"/etc/nginx/servers",
|
||||||
|
"/etc/nginx/conf.d",
|
||||||
|
}
|
||||||
|
XrayBinarySearchPaths = []string{
|
||||||
|
"/usr/local/bin/xray",
|
||||||
|
"/usr/bin/xray",
|
||||||
|
"/opt/xray/xray",
|
||||||
|
}
|
||||||
|
NginxBinarySearchPaths = []string{
|
||||||
|
"/usr/local/nginx/sbin/nginx",
|
||||||
|
"nginx",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
PathHealth = "/health"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PathChildTraffic = "/api/child/traffic"
|
||||||
|
PathChildSpeed = "/api/child/speed"
|
||||||
|
PathChildServiceStats = "/api/child/services/status"
|
||||||
|
PathChildServiceCtl = "/api/child/services/control"
|
||||||
|
PathChildXrayInstall = "/api/child/xray/install"
|
||||||
|
PathChildXrayRemove = "/api/child/xray/remove"
|
||||||
|
PathChildXrayConfig = "/api/child/xray/config"
|
||||||
|
PathChildXraySysCfg = "/api/child/xray/system-config"
|
||||||
|
PathChildXrayCfgFiles = "/api/child/xray/config-files"
|
||||||
|
PathChildNginxInstall = "/api/child/nginx/install"
|
||||||
|
PathChildNginxRemove = "/api/child/nginx/remove"
|
||||||
|
PathChildNginxConfig = "/api/child/nginx/config"
|
||||||
|
PathChildNginxCfgFile = "/api/child/nginx/config-files"
|
||||||
|
PathChildSystemInfo = "/api/child/system/info"
|
||||||
|
PathChildInbounds = "/api/child/inbounds"
|
||||||
|
PathChildOutbounds = "/api/child/outbounds"
|
||||||
|
PathChildRouting = "/api/child/routing"
|
||||||
|
PathChildScan = "/api/child/scan"
|
||||||
|
PathChildCertDeploy = "/api/child/cert/deploy"
|
||||||
|
PathChildNginxSetup = "/api/child/nginx/setup-ssl"
|
||||||
|
PathChildDomainProbe = "/api/child/domains/latency"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PathChildXrayInstallStream = "/api/child/xray/install-stream"
|
||||||
|
PathChildXrayRemoveStream = "/api/child/xray/remove-stream"
|
||||||
|
PathChildNginxInstallSSE = "/api/child/nginx/install-stream"
|
||||||
|
PathChildNginxRemoveSSE = "/api/child/nginx/remove-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PathRemoteWebSocket = "/api/remote/ws"
|
||||||
|
PathRemoteHeartbeat = "/api/remote/heartbeat"
|
||||||
|
PathRemoteTraffic = "/api/remote/traffic"
|
||||||
|
PathRemoteSpeed = "/api/remote/speed"
|
||||||
|
)
|
||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DomainLatencyProbeRequest struct {
|
type DomainLatencyProbeRequest struct {
|
||||||
@@ -24,7 +26,7 @@ type DomainLatencyProbeResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleDomainLatencyProbe handles POST /api/child/domains/latency
|
// 处理 POST /api/child/domains/latency 请求。
|
||||||
func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -49,13 +51,13 @@ func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
timeoutMs := req.TimeoutMs
|
timeoutMs := req.TimeoutMs
|
||||||
if timeoutMs <= 0 {
|
if timeoutMs <= 0 {
|
||||||
timeoutMs = 2000
|
timeoutMs = constants.DomainProbeDefaultTimeoutMS
|
||||||
}
|
}
|
||||||
if timeoutMs < 200 {
|
if timeoutMs < constants.DomainProbeMinTimeoutMS {
|
||||||
timeoutMs = 200
|
timeoutMs = constants.DomainProbeMinTimeoutMS
|
||||||
}
|
}
|
||||||
if timeoutMs > 10000 {
|
if timeoutMs > constants.DomainProbeMaxTimeoutMS {
|
||||||
timeoutMs = 10000
|
timeoutMs = constants.DomainProbeMaxTimeoutMS
|
||||||
}
|
}
|
||||||
timeout := time.Duration(timeoutMs) * time.Millisecond
|
timeout := time.Duration(timeoutMs) * time.Millisecond
|
||||||
|
|
||||||
@@ -64,13 +66,13 @@ func (h *ManageHandler) HandleDomainLatencyProbe(w http.ResponseWriter, r *http.
|
|||||||
writeError(w, http.StatusBadRequest, "no valid domain to probe")
|
writeError(w, http.StatusBadRequest, "no valid domain to probe")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(domains) > 200 {
|
if len(domains) > constants.DomainProbeMaxCount {
|
||||||
domains = domains[:200]
|
domains = domains[:constants.DomainProbeMaxCount]
|
||||||
}
|
}
|
||||||
|
|
||||||
results := make([]DomainLatencyProbeResult, 0, len(domains))
|
results := make([]DomainLatencyProbeResult, 0, len(domains))
|
||||||
resultCh := make(chan DomainLatencyProbeResult, len(domains))
|
resultCh := make(chan DomainLatencyProbeResult, len(domains))
|
||||||
sem := make(chan struct{}, 16)
|
sem := make(chan struct{}, constants.DomainProbeConcurrency)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@@ -206,7 +208,7 @@ func splitHostPortLoose(input string) (host string, port string, ok bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback for "domain:443" without brackets handling.
|
// 兼容不带方括号的 "domain:443" 写法。
|
||||||
idx := strings.LastIndex(s, ":")
|
idx := strings.LastIndex(s, ":")
|
||||||
if idx <= 0 || idx >= len(s)-1 {
|
if idx <= 0 || idx >= len(s)-1 {
|
||||||
return "", "", false
|
return "", "", false
|
||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"mmw-agent/internal/config"
|
"mmw-agent/internal/constants"
|
||||||
"mmw-agent/internal/xrpc"
|
"mmw-agent/internal/xrpc"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/app/proxyman/command"
|
"github.com/xtls/xray-core/app/proxyman/command"
|
||||||
@@ -26,21 +26,21 @@ import (
|
|||||||
|
|
||||||
var nginxInstalling atomic.Bool
|
var nginxInstalling atomic.Bool
|
||||||
|
|
||||||
// ManageHandler handles management API requests for child servers
|
// ManageHandler 处理子端管理接口请求。
|
||||||
type ManageHandler struct {
|
type ManageHandler struct {
|
||||||
configToken string
|
configToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManageHandler creates a new management handler
|
// 创建管理处理器。
|
||||||
func NewManageHandler(configToken string) *ManageHandler {
|
func NewManageHandler(configToken string) *ManageHandler {
|
||||||
return &ManageHandler{
|
return &ManageHandler{
|
||||||
configToken: configToken,
|
configToken: configToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate checks if the request is authorized (token + User-Agent)
|
// 校验请求身份(token + User-Agent)。
|
||||||
func (h *ManageHandler) authenticate(r *http.Request) bool {
|
func (h *ManageHandler) authenticate(r *http.Request) bool {
|
||||||
if r.Header.Get("User-Agent") != config.AgentUserAgent {
|
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,30 +48,30 @@ func (h *ManageHandler) authenticate(r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
auth := r.Header.Get("Authorization")
|
auth := r.Header.Get(constants.HeaderAuthorization)
|
||||||
if auth == "" {
|
if auth == "" {
|
||||||
auth = r.Header.Get("MM-Remote-Token")
|
auth = r.Header.Get(constants.HeaderMMRemoteToken)
|
||||||
}
|
}
|
||||||
if auth == "" {
|
if auth == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(auth, "Bearer ") {
|
if strings.HasPrefix(auth, constants.BearerPrefix) {
|
||||||
token := strings.TrimPrefix(auth, "Bearer ")
|
token := strings.TrimPrefix(auth, constants.BearerPrefix)
|
||||||
return token == h.configToken
|
return token == h.configToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth == h.configToken
|
return auth == h.configToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeJSON writes JSON response
|
// 输出 JSON 响应。
|
||||||
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeError writes error response
|
// 输出错误响应。
|
||||||
func writeError(w http.ResponseWriter, statusCode int, message string) {
|
func writeError(w http.ResponseWriter, statusCode int, message string) {
|
||||||
writeJSON(w, statusCode, map[string]interface{}{
|
writeJSON(w, statusCode, map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -79,23 +79,23 @@ func writeError(w http.ResponseWriter, statusCode int, message string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== System Services Status ==================
|
// ================== 系统服务状态 ==================
|
||||||
|
|
||||||
// ServicesStatusResponse represents the response for services status
|
// ServicesStatusResponse 表示服务状态查询响应。
|
||||||
type ServicesStatusResponse struct {
|
type ServicesStatusResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Xray *ServiceStatus `json:"xray,omitempty"`
|
Xray *ServiceStatus `json:"xray,omitempty"`
|
||||||
Nginx *ServiceStatus `json:"nginx,omitempty"`
|
Nginx *ServiceStatus `json:"nginx,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStatus represents a service status
|
// ServiceStatus 表示单个服务状态。
|
||||||
type ServiceStatus struct {
|
type ServiceStatus struct {
|
||||||
Installed bool `json:"installed"`
|
Installed bool `json:"installed"`
|
||||||
Running bool `json:"running"`
|
Running bool `json:"running"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleServicesStatus handles GET /api/child/services/status
|
// 处理 GET /api/child/services/status。
|
||||||
func (h *ManageHandler) HandleServicesStatus(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleServicesStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -121,8 +121,7 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
|
|||||||
|
|
||||||
xrayPath, err := exec.LookPath("xray")
|
xrayPath, err := exec.LookPath("xray")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
commonPaths := []string{"/usr/local/bin/xray", "/usr/bin/xray", "/opt/xray/xray"}
|
for _, p := range constants.XrayBinarySearchPaths {
|
||||||
for _, p := range commonPaths {
|
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
xrayPath = p
|
xrayPath = p
|
||||||
break
|
break
|
||||||
@@ -142,7 +141,7 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check systemctl first
|
// 优先使用 systemctl 检查
|
||||||
cmd := exec.Command("systemctl", "is-active", "xray")
|
cmd := exec.Command("systemctl", "is-active", "xray")
|
||||||
output, _ := cmd.Output()
|
output, _ := cmd.Output()
|
||||||
if strings.TrimSpace(string(output)) == "active" {
|
if strings.TrimSpace(string(output)) == "active" {
|
||||||
@@ -150,14 +149,14 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check if xray process is running via pgrep
|
// 兜底:用 pgrep 检查 xray 进程
|
||||||
pgrepCmd := exec.Command("pgrep", "-x", "xray")
|
pgrepCmd := exec.Command("pgrep", "-x", "xray")
|
||||||
if err := pgrepCmd.Run(); err == nil {
|
if err := pgrepCmd.Run(); err == nil {
|
||||||
status.Running = true
|
status.Running = true
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check via ps for processes containing "xray"
|
// 兜底:用 ps 检查包含 "xray" 的进程
|
||||||
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E '[x]ray' | head -1")
|
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E '[x]ray' | head -1")
|
||||||
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
status.Running = true
|
status.Running = true
|
||||||
@@ -169,12 +168,17 @@ func (h *ManageHandler) getXrayStatus() *ServiceStatus {
|
|||||||
func (h *ManageHandler) getNginxStatus() *ServiceStatus {
|
func (h *ManageHandler) getNginxStatus() *ServiceStatus {
|
||||||
status := &ServiceStatus{}
|
status := &ServiceStatus{}
|
||||||
|
|
||||||
// Check PATH first, then compiled install path
|
// 先查 PATH,再查编译安装路径
|
||||||
nginxPath, err := exec.LookPath("nginx")
|
nginxPath, err := exec.LookPath("nginx")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, statErr := os.Stat("/usr/local/nginx/sbin/nginx"); statErr == nil {
|
for _, candidate := range constants.NginxBinarySearchPaths {
|
||||||
nginxPath = "/usr/local/nginx/sbin/nginx"
|
if strings.Contains(candidate, "/") {
|
||||||
err = nil
|
if _, statErr := os.Stat(candidate); statErr == nil {
|
||||||
|
nginxPath = candidate
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -190,7 +194,7 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
|
|||||||
status.Version = "安装中..."
|
status.Version = "安装中..."
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check systemctl first
|
// 优先使用 systemctl 检查
|
||||||
cmd := exec.Command("systemctl", "is-active", "nginx")
|
cmd := exec.Command("systemctl", "is-active", "nginx")
|
||||||
output, _ := cmd.Output()
|
output, _ := cmd.Output()
|
||||||
if strings.TrimSpace(string(output)) == "active" {
|
if strings.TrimSpace(string(output)) == "active" {
|
||||||
@@ -198,14 +202,14 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check if nginx process is running via pgrep
|
// 兜底:用 pgrep 检查 nginx 进程
|
||||||
pgrepCmd := exec.Command("pgrep", "-x", "nginx")
|
pgrepCmd := exec.Command("pgrep", "-x", "nginx")
|
||||||
if err := pgrepCmd.Run(); err == nil {
|
if err := pgrepCmd.Run(); err == nil {
|
||||||
status.Running = true
|
status.Running = true
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check via ps for nginx master process
|
// 兜底:用 ps 检查 nginx master 进程
|
||||||
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E 'nginx: master' | head -1")
|
psCmd := exec.Command("bash", "-c", "ps aux | grep -v grep | grep -E 'nginx: master' | head -1")
|
||||||
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
if output, err := psCmd.Output(); err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
status.Running = true
|
status.Running = true
|
||||||
@@ -214,15 +218,15 @@ func (h *ManageHandler) getNginxStatus() *ServiceStatus {
|
|||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Service Control ==================
|
// ================== 服务控制 ==================
|
||||||
|
|
||||||
// ServiceControlRequest represents a service control request
|
// ServiceControlRequest 表示服务控制请求。
|
||||||
type ServiceControlRequest struct {
|
type ServiceControlRequest struct {
|
||||||
Service string `json:"service"`
|
Service string `json:"service"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleServiceControl handles POST /api/child/services/control
|
// 处理 POST /api/child/services/control。
|
||||||
func (h *ManageHandler) HandleServiceControl(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleServiceControl(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -265,9 +269,9 @@ func (h *ManageHandler) HandleServiceControl(w http.ResponseWriter, r *http.Requ
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Installation ==================
|
// ================== Xray 安装 ==================
|
||||||
|
|
||||||
// HandleXrayInstall handles POST /api/child/xray/install
|
// 处理 POST /api/child/xray/install。
|
||||||
func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -297,7 +301,7 @@ func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
log.Printf("[Manage] Xray installed successfully")
|
log.Printf("[Manage] Xray installed successfully")
|
||||||
|
|
||||||
// Deploy default config if no config exists
|
// 若无配置则下发默认配置
|
||||||
h.deployDefaultXrayConfig()
|
h.deployDefaultXrayConfig()
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
@@ -307,7 +311,7 @@ func (h *ManageHandler) HandleXrayInstall(w http.ResponseWriter, r *http.Request
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleXrayRemove handles POST /api/child/xray/remove
|
// 处理 POST /api/child/xray/remove。
|
||||||
func (h *ManageHandler) HandleXrayRemove(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleXrayRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -344,9 +348,9 @@ func (h *ManageHandler) HandleXrayRemove(w http.ResponseWriter, r *http.Request)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Configuration ==================
|
// ================== Xray 配置 ==================
|
||||||
|
|
||||||
// HandleXrayConfig handles GET/POST /api/child/xray/config
|
// 处理 GET/POST /api/child/xray/config。
|
||||||
func (h *ManageHandler) HandleXrayConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleXrayConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -364,11 +368,7 @@ func (h *ManageHandler) HandleXrayConfig(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ManageHandler) getXrayConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) getXrayConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
configPaths := []string{
|
configPaths := constants.DefaultXrayConfigPaths
|
||||||
"/usr/local/etc/xray/config.json",
|
|
||||||
"/etc/xray/config.json",
|
|
||||||
"/opt/xray/config.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
var configPath string
|
var configPath string
|
||||||
var content []byte
|
var content []byte
|
||||||
@@ -413,7 +413,7 @@ func (h *ManageHandler) setXrayConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
configPath := req.Path
|
configPath := req.Path
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "/usr/local/etc/xray/config.json"
|
configPath = constants.DefaultXrayConfigPaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.Dir(configPath)
|
dir := filepath.Dir(configPath)
|
||||||
@@ -436,9 +436,9 @@ func (h *ManageHandler) setXrayConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray System Configuration ==================
|
// ================== Xray 系统配置 ==================
|
||||||
|
|
||||||
// XraySystemConfig represents the system configuration state
|
// XraySystemConfig 表示 Xray 系统配置状态。
|
||||||
type XraySystemConfig struct {
|
type XraySystemConfig struct {
|
||||||
MetricsEnabled bool `json:"metrics_enabled"`
|
MetricsEnabled bool `json:"metrics_enabled"`
|
||||||
MetricsListen string `json:"metrics_listen"`
|
MetricsListen string `json:"metrics_listen"`
|
||||||
@@ -447,7 +447,7 @@ type XraySystemConfig struct {
|
|||||||
GrpcPort int `json:"grpc_port"`
|
GrpcPort int `json:"grpc_port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleXraySystemConfig handles GET/POST /api/child/xray/system-config
|
// 处理 GET/POST /api/child/xray/system-config。
|
||||||
func (h *ManageHandler) HandleXraySystemConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleXraySystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -484,7 +484,7 @@ func (h *ManageHandler) getXraySystemConfig(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
sysConfig := &XraySystemConfig{
|
sysConfig := &XraySystemConfig{
|
||||||
MetricsListen: "127.0.0.1:38889",
|
MetricsListen: constants.DefaultMetricsListen,
|
||||||
GrpcPort: 46736,
|
GrpcPort: 46736,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,10 +605,10 @@ func (h *ManageHandler) updateXraySystemConfig(w http.ResponseWriter, r *http.Re
|
|||||||
apiInbound := map[string]interface{}{
|
apiInbound := map[string]interface{}{
|
||||||
"tag": "api",
|
"tag": "api",
|
||||||
"port": float64(req.GrpcPort),
|
"port": float64(req.GrpcPort),
|
||||||
"listen": "127.0.0.1",
|
"listen": constants.LocalhostIP,
|
||||||
"protocol": "dokodemo-door",
|
"protocol": "dokodemo-door",
|
||||||
"settings": map[string]interface{}{
|
"settings": map[string]interface{}{
|
||||||
"address": "127.0.0.1",
|
"address": constants.LocalhostIP,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
|
config["inbounds"] = append([]interface{}{apiInbound}, inbounds...)
|
||||||
@@ -618,10 +618,10 @@ func (h *ManageHandler) updateXraySystemConfig(w http.ResponseWriter, r *http.Re
|
|||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"tag": "api",
|
"tag": "api",
|
||||||
"port": float64(req.GrpcPort),
|
"port": float64(req.GrpcPort),
|
||||||
"listen": "127.0.0.1",
|
"listen": constants.LocalhostIP,
|
||||||
"protocol": "dokodemo-door",
|
"protocol": "dokodemo-door",
|
||||||
"settings": map[string]interface{}{
|
"settings": map[string]interface{}{
|
||||||
"address": "127.0.0.1",
|
"address": constants.LocalhostIP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -722,9 +722,9 @@ func (h *ManageHandler) removeAPIRoutingRule(config map[string]interface{}) {
|
|||||||
routing["rules"] = newRules
|
routing["rules"] = newRules
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Nginx Installation ==================
|
// ================== Nginx 安装 ==================
|
||||||
|
|
||||||
// HandleNginxInstall handles POST /api/child/nginx/install
|
// 处理 POST /api/child/nginx/install。
|
||||||
func (h *ManageHandler) HandleNginxInstall(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleNginxInstall(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -777,7 +777,7 @@ func (h *ManageHandler) HandleNginxInstall(w http.ResponseWriter, r *http.Reques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleNginxRemove handles POST /api/child/nginx/remove
|
// 处理 POST /api/child/nginx/remove。
|
||||||
func (h *ManageHandler) HandleNginxRemove(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleNginxRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -814,9 +814,9 @@ func (h *ManageHandler) HandleNginxRemove(w http.ResponseWriter, r *http.Request
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Nginx Configuration ==================
|
// ================== Nginx 配置 ==================
|
||||||
|
|
||||||
// HandleNginxConfig handles GET/POST /api/child/nginx/config
|
// 处理 GET/POST /api/child/nginx/config。
|
||||||
func (h *ManageHandler) HandleNginxConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleNginxConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -834,10 +834,7 @@ func (h *ManageHandler) HandleNginxConfig(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ManageHandler) getNginxConfig(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) getNginxConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
configPaths := []string{
|
configPaths := constants.DefaultNginxConfigPaths
|
||||||
"/etc/nginx/nginx.conf",
|
|
||||||
"/usr/local/nginx/conf/nginx.conf",
|
|
||||||
}
|
|
||||||
|
|
||||||
var configPath string
|
var configPath string
|
||||||
var content []byte
|
var content []byte
|
||||||
@@ -876,7 +873,7 @@ func (h *ManageHandler) setNginxConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
configPath := req.Path
|
configPath := req.Path
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "/etc/nginx/nginx.conf"
|
configPath = constants.DefaultNginxConfigPaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
backupPath := configPath + ".bak." + time.Now().Format("20060102150405")
|
backupPath := configPath + ".bak." + time.Now().Format("20060102150405")
|
||||||
@@ -908,9 +905,9 @@ func (h *ManageHandler) setNginxConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== System Info ==================
|
// ================== 系统信息 ==================
|
||||||
|
|
||||||
// HandleSystemInfo handles GET /api/child/system/info
|
// 处理 GET /api/child/system/info。
|
||||||
func (h *ManageHandler) HandleSystemInfo(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleSystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -960,9 +957,9 @@ func (h *ManageHandler) HandleSystemInfo(w http.ResponseWriter, r *http.Request)
|
|||||||
writeJSON(w, http.StatusOK, info)
|
writeJSON(w, http.StatusOK, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Config Files Management ==================
|
// ================== 配置文件管理 ==================
|
||||||
|
|
||||||
// ConfigFileInfo represents a config file entry
|
// ConfigFileInfo 表示配置文件条目。
|
||||||
type ConfigFileInfo struct {
|
type ConfigFileInfo struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -970,7 +967,7 @@ type ConfigFileInfo struct {
|
|||||||
ModTime string `json:"mod_time"`
|
ModTime string `json:"mod_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleXrayConfigFiles handles listing and managing xray config files
|
// 处理 xray 配置文件的列表与读写。
|
||||||
func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -993,11 +990,7 @@ func (h *ManageHandler) HandleXrayConfigFiles(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ManageHandler) listXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) listXrayConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
configDirs := []string{
|
configDirs := constants.XrayConfigDirPaths
|
||||||
"/usr/local/etc/xray",
|
|
||||||
"/etc/xray",
|
|
||||||
"/opt/xray",
|
|
||||||
}
|
|
||||||
|
|
||||||
var files []ConfigFileInfo
|
var files []ConfigFileInfo
|
||||||
var baseDir string
|
var baseDir string
|
||||||
@@ -1043,9 +1036,9 @@ func (h *ManageHandler) getXrayConfigFile(w http.ResponseWriter, r *http.Request
|
|||||||
file = filepath.Clean(file)
|
file = filepath.Clean(file)
|
||||||
|
|
||||||
configDirs := []string{
|
configDirs := []string{
|
||||||
"/usr/local/etc/xray",
|
constants.XrayConfigDirPaths[0],
|
||||||
"/etc/xray",
|
constants.XrayConfigDirPaths[1],
|
||||||
"/opt/xray",
|
constants.XrayConfigDirPaths[2],
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath string
|
var filePath string
|
||||||
@@ -1100,9 +1093,9 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDirs := []string{
|
configDirs := []string{
|
||||||
"/usr/local/etc/xray",
|
constants.XrayConfigDirPaths[0],
|
||||||
"/etc/xray",
|
constants.XrayConfigDirPaths[1],
|
||||||
"/opt/xray",
|
constants.XrayConfigDirPaths[2],
|
||||||
}
|
}
|
||||||
|
|
||||||
var configDir string
|
var configDir string
|
||||||
@@ -1114,7 +1107,7 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if configDir == "" {
|
if configDir == "" {
|
||||||
configDir = "/usr/local/etc/xray"
|
configDir = constants.XrayConfigDirPaths[0]
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create config directory: %v", err))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create config directory: %v", err))
|
||||||
return
|
return
|
||||||
@@ -1145,7 +1138,7 @@ func (h *ManageHandler) saveXrayConfigFile(w http.ResponseWriter, r *http.Reques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleNginxConfigFiles handles listing and managing nginx config files
|
// 处理 nginx 配置文件的列表与读写。
|
||||||
func (h *ManageHandler) HandleNginxConfigFiles(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleNginxConfigFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -1172,10 +1165,10 @@ func (h *ManageHandler) listNginxConfigFiles(w http.ResponseWriter, r *http.Requ
|
|||||||
dir string
|
dir string
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
{"/etc/nginx", "main"},
|
{constants.NginxConfigDirPaths[0], "main"},
|
||||||
{"/etc/nginx/sites-available", "sites-available"},
|
{constants.NginxConfigDirPaths[1], "sites-available"},
|
||||||
{"/etc/nginx/sites-enabled", "sites-enabled"},
|
{constants.NginxConfigDirPaths[2], "sites-enabled"},
|
||||||
{"/etc/nginx/conf.d", "conf.d"},
|
{constants.NginxConfigDirPaths[3], "conf.d"},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string][]ConfigFileInfo)
|
result := make(map[string][]ConfigFileInfo)
|
||||||
@@ -1218,11 +1211,11 @@ func (h *ManageHandler) getNginxConfigFile(w http.ResponseWriter, r *http.Reques
|
|||||||
file = filepath.Clean(file)
|
file = filepath.Clean(file)
|
||||||
|
|
||||||
allowedDirs := []string{
|
allowedDirs := []string{
|
||||||
"/etc/nginx",
|
constants.NginxConfigDirPaths[0],
|
||||||
"/etc/nginx/sites-available",
|
constants.NginxConfigDirPaths[1],
|
||||||
"/etc/nginx/sites-enabled",
|
constants.NginxConfigDirPaths[2],
|
||||||
"/etc/nginx/conf.d",
|
constants.NginxConfigDirPaths[3],
|
||||||
"/usr/local/nginx/conf",
|
constants.NginxConfigDirPaths[4],
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath string
|
var filePath string
|
||||||
@@ -1283,8 +1276,8 @@ func (h *ManageHandler) saveNginxConfigFile(w http.ResponseWriter, r *http.Reque
|
|||||||
req.Path = filepath.Clean(req.Path)
|
req.Path = filepath.Clean(req.Path)
|
||||||
|
|
||||||
allowedDirs := []string{
|
allowedDirs := []string{
|
||||||
"/etc/nginx",
|
constants.NginxConfigDirPaths[0],
|
||||||
"/usr/local/nginx/conf",
|
constants.NginxConfigDirPaths[4],
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed := false
|
allowed := false
|
||||||
@@ -1338,16 +1331,16 @@ func (h *ManageHandler) saveNginxConfigFile(w http.ResponseWriter, r *http.Reque
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Inbounds Management ==================
|
// ================== Xray 入站管理 ==================
|
||||||
|
|
||||||
// InboundRequest represents inbound management request
|
// InboundRequest 表示入站管理请求。
|
||||||
type InboundRequest struct {
|
type InboundRequest struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Inbound map[string]interface{} `json:"inbound,omitempty"`
|
Inbound map[string]interface{} `json:"inbound,omitempty"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleInbounds handles inbound management
|
// 处理入站管理请求。
|
||||||
func (h *ManageHandler) HandleInbounds(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleInbounds(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -1412,7 +1405,7 @@ func (h *ManageHandler) getInboundTagsFromGRPC() []string {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
|
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Manage] Failed to connect to Xray gRPC: %v", err)
|
log.Printf("[Manage] Failed to connect to Xray gRPC: %v", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -1517,7 +1510,7 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
|
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
|
||||||
return
|
return
|
||||||
@@ -1557,27 +1550,27 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to remove from runtime (ignore error if not running)
|
// 尝试从运行态移除(未运行时报错可忽略)
|
||||||
runtimeErr := h.removeInbound(ctx, clients.Handler, req.Tag)
|
runtimeErr := h.removeInbound(ctx, clients.Handler, req.Tag)
|
||||||
if runtimeErr != nil {
|
if runtimeErr != nil {
|
||||||
log.Printf("[Manage] Warning: Failed to remove inbound from runtime: %v", runtimeErr)
|
log.Printf("[Manage] Warning: Failed to remove inbound from runtime: %v", runtimeErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from config file (this is the primary operation)
|
// 从配置文件移除(主流程)
|
||||||
configErr := h.removeInboundFromConfig(req.Tag)
|
configErr := h.removeInboundFromConfig(req.Tag)
|
||||||
if configErr != nil {
|
if configErr != nil {
|
||||||
log.Printf("[Manage] Warning: Failed to remove inbound from config: %v", configErr)
|
log.Printf("[Manage] Warning: Failed to remove inbound from config: %v", configErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success if config operation succeeded (runtime removal is optional)
|
// 配置文件操作成功即可视为成功(运行态移除可选)
|
||||||
// The inbound might not exist in runtime if Xray wasn't restarted after config change
|
// 配置改动后若未重启,运行态可能还没有该入站
|
||||||
if configErr != nil {
|
if configErr != nil {
|
||||||
// Config file operation failed
|
// 配置文件操作失败
|
||||||
if runtimeErr != nil {
|
if runtimeErr != nil {
|
||||||
// Both failed - check if it's just "not found" errors
|
// 两边都失败时,判断是否只是“未找到”错误
|
||||||
if strings.Contains(runtimeErr.Error(), "not enough information") {
|
if strings.Contains(runtimeErr.Error(), "not enough information") {
|
||||||
// Xray says the inbound doesn't exist in runtime, which is fine
|
// Xray 返回运行态不存在该入站,这属于可接受情况
|
||||||
// Just report config error
|
// 仅返回配置文件错误
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound from config: %v", configErr))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound from config: %v", configErr))
|
||||||
} else {
|
} else {
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound: runtime=%v, config=%v", runtimeErr, configErr))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to remove inbound: runtime=%v, config=%v", runtimeErr, configErr))
|
||||||
@@ -1588,7 +1581,7 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config succeeded, runtime error is acceptable (inbound might not be loaded)
|
// 配置成功时,运行态报错可接受(可能尚未加载)
|
||||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Inbound removed successfully",
|
"message": "Inbound removed successfully",
|
||||||
@@ -1599,16 +1592,16 @@ func (h *ManageHandler) manageInbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Outbounds Management ==================
|
// ================== Xray 出站管理 ==================
|
||||||
|
|
||||||
// OutboundRequest represents outbound management request
|
// OutboundRequest 表示出站管理请求。
|
||||||
type OutboundRequest struct {
|
type OutboundRequest struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Outbound map[string]interface{} `json:"outbound,omitempty"`
|
Outbound map[string]interface{} `json:"outbound,omitempty"`
|
||||||
Tag string `json:"tag,omitempty"`
|
Tag string `json:"tag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleOutbounds handles outbound management
|
// 处理出站管理请求。
|
||||||
func (h *ManageHandler) HandleOutbounds(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleOutbounds(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -1673,7 +1666,7 @@ func (h *ManageHandler) manageOutbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
clients, err := xrpc.New(ctx, "127.0.0.1", uint16(apiPort))
|
clients, err := xrpc.New(ctx, constants.LocalhostIP, uint16(apiPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to connect to Xray: %v", err))
|
||||||
return
|
return
|
||||||
@@ -1726,9 +1719,9 @@ func (h *ManageHandler) manageOutbound(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Routing Management ==================
|
// ================== Xray 路由管理 ==================
|
||||||
|
|
||||||
// RoutingRequest represents routing management request
|
// RoutingRequest 表示路由管理请求。
|
||||||
type RoutingRequest struct {
|
type RoutingRequest struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Routing map[string]interface{} `json:"routing,omitempty"`
|
Routing map[string]interface{} `json:"routing,omitempty"`
|
||||||
@@ -1736,7 +1729,7 @@ type RoutingRequest struct {
|
|||||||
Index int `json:"index,omitempty"`
|
Index int `json:"index,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleRouting handles routing management
|
// 处理路由管理请求。
|
||||||
func (h *ManageHandler) HandleRouting(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleRouting(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
writeError(w, http.StatusUnauthorized, "Unauthorized")
|
||||||
@@ -1871,14 +1864,10 @@ func (h *ManageHandler) manageRouting(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Helper Functions ==================
|
// ================== 辅助函数 ==================
|
||||||
|
|
||||||
func (h *ManageHandler) findXrayConfigPath() string {
|
func (h *ManageHandler) findXrayConfigPath() string {
|
||||||
configPaths := []string{
|
configPaths := constants.DefaultXrayConfigPaths
|
||||||
"/usr/local/etc/xray/config.json",
|
|
||||||
"/etc/xray/config.json",
|
|
||||||
"/opt/xray/config.json",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range configPaths {
|
for _, p := range configPaths {
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
@@ -2124,9 +2113,9 @@ func (h *ManageHandler) removeOutboundFromConfig(tag string) error {
|
|||||||
return os.WriteFile(configPath, newContent, 0644)
|
return os.WriteFile(configPath, newContent, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Scan ==================
|
// ================== 扫描 ==================
|
||||||
|
|
||||||
// ScanResponse represents the response for scan operation
|
// ScanResponse 表示扫描接口响应。
|
||||||
type ScanResponse struct {
|
type ScanResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -2139,7 +2128,7 @@ type ScanResponse struct {
|
|||||||
ConfigAddedSections []string `json:"config_added_sections,omitempty"`
|
ConfigAddedSections []string `json:"config_added_sections,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleScan handles POST /api/child/scan
|
// 处理 POST /api/child/scan。
|
||||||
func (h *ManageHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -2220,9 +2209,9 @@ func (h *ManageHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, response)
|
writeJSON(w, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Xray Config Auto-Complete ==================
|
// ================== Xray 配置自动补全 ==================
|
||||||
|
|
||||||
// EnsureXrayConfigResult holds the result of config check
|
// EnsureXrayConfigResult 表示配置检查结果。
|
||||||
type EnsureXrayConfigResult struct {
|
type EnsureXrayConfigResult struct {
|
||||||
ConfigPath string `json:"config_path"`
|
ConfigPath string `json:"config_path"`
|
||||||
Modified bool `json:"modified"`
|
Modified bool `json:"modified"`
|
||||||
@@ -2230,7 +2219,7 @@ type EnsureXrayConfigResult struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureXrayConfig checks and completes Xray configuration
|
// 检查并补全 Xray 配置。
|
||||||
func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
|
func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
|
||||||
result := &EnsureXrayConfigResult{}
|
result := &EnsureXrayConfigResult{}
|
||||||
|
|
||||||
@@ -2279,7 +2268,7 @@ func (h *ManageHandler) EnsureXrayConfig() *EnsureXrayConfigResult {
|
|||||||
if _, ok := config["metrics"]; !ok {
|
if _, ok := config["metrics"]; !ok {
|
||||||
config["metrics"] = map[string]interface{}{
|
config["metrics"] = map[string]interface{}{
|
||||||
"tag": "Metrics",
|
"tag": "Metrics",
|
||||||
"listen": "127.0.0.1:38889",
|
"listen": constants.DefaultMetricsListen,
|
||||||
}
|
}
|
||||||
result.AddedSections = append(result.AddedSections, "metrics")
|
result.AddedSections = append(result.AddedSections, "metrics")
|
||||||
modified = true
|
modified = true
|
||||||
@@ -2376,10 +2365,10 @@ func (h *ManageHandler) addAPIInbound(config map[string]interface{}) {
|
|||||||
apiInbound := map[string]interface{}{
|
apiInbound := map[string]interface{}{
|
||||||
"tag": "api",
|
"tag": "api",
|
||||||
"port": float64(46736),
|
"port": float64(46736),
|
||||||
"listen": "127.0.0.1",
|
"listen": constants.LocalhostIP,
|
||||||
"protocol": "dokodemo-door",
|
"protocol": "dokodemo-door",
|
||||||
"settings": map[string]interface{}{
|
"settings": map[string]interface{}{
|
||||||
"address": "127.0.0.1",
|
"address": constants.LocalhostIP,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2432,9 +2421,9 @@ func (h *ManageHandler) addAPIRoutingRule(config map[string]interface{}) {
|
|||||||
routing["rules"] = append([]interface{}{apiRule}, rules...)
|
routing["rules"] = append([]interface{}{apiRule}, rules...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== Certificate Deploy ==================
|
// ================== 证书部署 ==================
|
||||||
|
|
||||||
// CertDeployRequest represents a certificate deploy request from master
|
// CertDeployRequest 表示主控端下发的证书部署请求。
|
||||||
type CertDeployRequest struct {
|
type CertDeployRequest struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
CertPEM string `json:"cert_pem"`
|
CertPEM string `json:"cert_pem"`
|
||||||
@@ -2444,7 +2433,7 @@ type CertDeployRequest struct {
|
|||||||
Reload string `json:"reload"` // nginx, xray, both, none
|
Reload string `json:"reload"` // nginx, xray, both, none
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleCertDeploy handles POST /api/child/cert/deploy
|
// 处理 POST /api/child/cert/deploy。
|
||||||
func (h *ManageHandler) HandleCertDeploy(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleCertDeploy(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
@@ -2513,9 +2502,9 @@ func deployNginxSSLConfig(domain string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
confDir := "/usr/local/nginx/conf"
|
confDir := constants.NginxConfigDirPaths[4]
|
||||||
if _, err := os.Stat(confDir); err != nil {
|
if _, err := os.Stat(confDir); err != nil {
|
||||||
confDir = "/etc/nginx"
|
confDir = constants.NginxConfigDirPaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
certDir := filepath.Join(confDir, "cert")
|
certDir := filepath.Join(confDir, "cert")
|
||||||
@@ -2539,7 +2528,7 @@ func deployNginxSSLConfig(domain string) {
|
|||||||
}
|
}
|
||||||
`, domain, domain, domain)
|
`, domain, domain, domain)
|
||||||
|
|
||||||
// Write to conf.d or append via include
|
// 写入 conf.d,或通过 include 挂载
|
||||||
confDDir := filepath.Join(confDir, "conf.d")
|
confDDir := filepath.Join(confDir, "conf.d")
|
||||||
os.MkdirAll(confDDir, 0755)
|
os.MkdirAll(confDDir, 0755)
|
||||||
sslConfPath := filepath.Join(confDDir, "ssl.conf")
|
sslConfPath := filepath.Join(confDDir, "ssl.conf")
|
||||||
@@ -2549,7 +2538,7 @@ func deployNginxSSLConfig(domain string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure main nginx.conf includes conf.d/*.conf
|
// 确保主 nginx.conf 包含 conf.d/*.conf
|
||||||
mainConf := filepath.Join(confDir, "nginx.conf")
|
mainConf := filepath.Join(confDir, "nginx.conf")
|
||||||
content, err := os.ReadFile(mainConf)
|
content, err := os.ReadFile(mainConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2559,7 +2548,7 @@ func deployNginxSSLConfig(domain string) {
|
|||||||
|
|
||||||
includeDirective := "include conf.d/*.conf;"
|
includeDirective := "include conf.d/*.conf;"
|
||||||
if !strings.Contains(string(content), includeDirective) {
|
if !strings.Contains(string(content), includeDirective) {
|
||||||
// Insert include before the last closing brace of http block
|
// 在 http 块最后一个右括号前插入 include
|
||||||
text := string(content)
|
text := string(content)
|
||||||
lastBrace := strings.LastIndex(text, "}")
|
lastBrace := strings.LastIndex(text, "}")
|
||||||
if lastBrace > 0 {
|
if lastBrace > 0 {
|
||||||
@@ -2574,8 +2563,8 @@ func deployNginxSSLConfig(domain string) {
|
|||||||
log.Printf("[Manage] Nginx SSL config deployed for domain %s at %s", domain, sslConfPath)
|
log.Printf("[Manage] Nginx SSL config deployed for domain %s at %s", domain, sslConfPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleNginxSetupSSL handles POST /api/child/nginx/setup-ssl
|
// HandleNginxSetupSSL 处理 POST /api/child/nginx/setup-ssl。
|
||||||
// Deploys nginx.conf + domain server block to servers/{domain}.conf.
|
// 部署 nginx.conf 和 servers/{domain}.conf。
|
||||||
func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Request) {
|
func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "Method not allowed")
|
||||||
@@ -2598,17 +2587,17 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
domain := strings.ToLower(strings.TrimSpace(req.Domain))
|
domain := strings.ToLower(strings.TrimSpace(req.Domain))
|
||||||
|
|
||||||
confDir := "/usr/local/nginx"
|
confDir := constants.NginxPrimaryPrefixDir
|
||||||
if _, err := os.Stat(confDir); err != nil {
|
if _, err := os.Stat(confDir); err != nil {
|
||||||
confDir = "/etc/nginx"
|
confDir = constants.NginxConfigDirPaths[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure cert and servers directories exist
|
// 确保证书和 servers 目录存在
|
||||||
os.MkdirAll(filepath.Join(confDir, "cert"), 0755)
|
os.MkdirAll(filepath.Join(confDir, "cert"), 0755)
|
||||||
os.MkdirAll(filepath.Join(confDir, "servers"), 0755)
|
os.MkdirAll(filepath.Join(confDir, "servers"), 0755)
|
||||||
|
|
||||||
if req.NginxConfig != "" {
|
if req.NginxConfig != "" {
|
||||||
// Deploy base nginx.conf
|
// 下发主 nginx.conf
|
||||||
mainConf := filepath.Join(confDir, "nginx.conf")
|
mainConf := filepath.Join(confDir, "nginx.conf")
|
||||||
if content, err := os.ReadFile(mainConf); err == nil {
|
if content, err := os.ReadFile(mainConf); err == nil {
|
||||||
os.WriteFile(mainConf+".bak."+time.Now().Format("20060102150405"), content, 0644)
|
os.WriteFile(mainConf+".bak."+time.Now().Format("20060102150405"), content, 0644)
|
||||||
@@ -2621,7 +2610,7 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.DomainConfig != "" {
|
if req.DomainConfig != "" {
|
||||||
// Deploy domain-specific server block to servers/{domain}.conf
|
// 下发域名 server 配置到 servers/{domain}.conf
|
||||||
domainConfPath := filepath.Join(confDir, "servers", domain+".conf")
|
domainConfPath := filepath.Join(confDir, "servers", domain+".conf")
|
||||||
if err := os.WriteFile(domainConfPath, []byte(req.DomainConfig), 0644); err != nil {
|
if err := os.WriteFile(domainConfPath, []byte(req.DomainConfig), 0644); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write domain config: %v", err))
|
writeError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to write domain config: %v", err))
|
||||||
@@ -2629,11 +2618,11 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
log.Printf("[Manage] Domain config deployed at %s", domainConfPath)
|
log.Printf("[Manage] Domain config deployed at %s", domainConfPath)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: legacy behavior
|
// 兜底:沿用旧逻辑
|
||||||
deployNginxSSLConfig(domain)
|
deployNginxSSLConfig(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload nginx to apply
|
// 重载 nginx 使配置生效
|
||||||
if err := reloadNginx(); err != nil {
|
if err := reloadNginx(); err != nil {
|
||||||
log.Printf("[Manage] Nginx reload after setup-ssl failed: %v", err)
|
log.Printf("[Manage] Nginx reload after setup-ssl failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -2645,7 +2634,7 @@ func (h *ManageHandler) HandleNginxSetupSSL(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
func reloadNginx() error {
|
func reloadNginx() error {
|
||||||
for _, bin := range []string{"/usr/local/nginx/sbin/nginx", "nginx"} {
|
for _, bin := range constants.NginxBinarySearchPaths {
|
||||||
if path, err := exec.LookPath(bin); err == nil {
|
if path, err := exec.LookPath(bin); err == nil {
|
||||||
return runCommand(path, "-s", "reload")
|
return runCommand(path, "-s", "reload")
|
||||||
}
|
}
|
||||||
@@ -2660,11 +2649,11 @@ func runCommand(name string, args ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deployDefaultXrayConfig deploys the embedded default xray config if no config exists.
|
// 在缺失配置时下发内置默认配置。
|
||||||
func (h *ManageHandler) deployDefaultXrayConfig() {
|
func (h *ManageHandler) deployDefaultXrayConfig() {
|
||||||
configPath := "/usr/local/etc/xray/config.json"
|
configPath := constants.DefaultXrayConfigPaths[0]
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
// Config already exists — run EnsureXrayConfig to add missing sections
|
// 配置已存在,执行 EnsureXrayConfig 补齐缺失段
|
||||||
result := h.EnsureXrayConfig()
|
result := h.EnsureXrayConfig()
|
||||||
if result.Modified {
|
if result.Modified {
|
||||||
log.Printf("[Manage] Xray config updated after install: added %v", result.AddedSections)
|
log.Printf("[Manage] Xray config updated after install: added %v", result.AddedSections)
|
||||||
@@ -2685,7 +2674,7 @@ func (h *ManageHandler) deployDefaultXrayConfig() {
|
|||||||
exec.Command("systemctl", "restart", "xray").Run()
|
exec.Command("systemctl", "restart", "xray").Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== SSE Streaming Install/Remove ==================
|
// ================== SSE 流式安装/卸载 ==================
|
||||||
|
|
||||||
func sseStreamCmd(w http.ResponseWriter, r *http.Request, cmd *exec.Cmd, completeMsg string) {
|
func sseStreamCmd(w http.ResponseWriter, r *http.Request, cmd *exec.Cmd, completeMsg string) {
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
@@ -2775,7 +2764,7 @@ func (h *ManageHandler) HandleXrayInstallStream(w http.ResponseWriter, r *http.R
|
|||||||
cmd.Env = os.Environ()
|
cmd.Env = os.Environ()
|
||||||
sseStreamCmd(w, r, cmd, "Xray installed successfully")
|
sseStreamCmd(w, r, cmd, "Xray installed successfully")
|
||||||
|
|
||||||
// Deploy default config after install
|
// 安装完成后下发默认配置
|
||||||
h.deployDefaultXrayConfig()
|
h.deployDefaultXrayConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,16 +7,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"mmw-agent/internal/agent"
|
"mmw-agent/internal/agent"
|
||||||
"mmw-agent/internal/config"
|
"mmw-agent/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIHandler handles API requests from the master server (for pull mode)
|
// APIHandler 处理来自主控端的请求(拉取模式)。
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
client *agent.Client
|
client *agent.Client
|
||||||
configToken string
|
configToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAPIHandler creates a new API handler
|
// 创建 API 处理器。
|
||||||
func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
|
func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
|
||||||
return &APIHandler{
|
return &APIHandler{
|
||||||
client: client,
|
client: client,
|
||||||
@@ -24,7 +24,7 @@ func NewAPIHandler(client *agent.Client, configToken string) *APIHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP handles the HTTP request for traffic data
|
// 返回流量数据。
|
||||||
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -32,7 +32,7 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -44,7 +44,7 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
stats, err := h.client.GetStats()
|
stats, err := h.client.GetStats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[API] Failed to get stats: %v", err)
|
log.Printf("[API] Failed to get stats: %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -53,14 +53,14 @@ func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeSpeedHTTP handles the HTTP request for speed data
|
// 返回速率数据。
|
||||||
func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -68,7 +68,7 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !h.authenticate(r) {
|
if !h.authenticate(r) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -79,7 +79,7 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
uploadSpeed, downloadSpeed := h.client.GetSpeed()
|
uploadSpeed, downloadSpeed := h.client.GetSpeed()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set(constants.HeaderContentType, constants.ContentTypeJSON)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"success": true,
|
"success": true,
|
||||||
"upload_speed": uploadSpeed,
|
"upload_speed": uploadSpeed,
|
||||||
@@ -87,9 +87,9 @@ func (h *APIHandler) ServeSpeedHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate checks if the request is authorized (token + User-Agent)
|
// 校验请求身份(token + User-Agent)。
|
||||||
func (h *APIHandler) authenticate(r *http.Request) bool {
|
func (h *APIHandler) authenticate(r *http.Request) bool {
|
||||||
if r.Header.Get("User-Agent") != config.AgentUserAgent {
|
if r.Header.Get(constants.HeaderUserAgent) != constants.AgentUserAgent {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +97,13 @@ func (h *APIHandler) authenticate(r *http.Request) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
auth := r.Header.Get("Authorization")
|
auth := r.Header.Get(constants.HeaderAuthorization)
|
||||||
if auth == "" {
|
if auth == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(auth, "Bearer ") {
|
if strings.HasPrefix(auth, constants.BearerPrefix) {
|
||||||
token := strings.TrimPrefix(auth, "Bearer ")
|
token := strings.TrimPrefix(auth, constants.BearerPrefix)
|
||||||
return token == h.configToken
|
return token == h.configToken
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册子端 API 路由
|
||||||
|
func RegisterChildRoutes(mux *http.ServeMux, apiHandler *APIHandler, manageHandler *ManageHandler) {
|
||||||
|
// 拉取模式数据接口
|
||||||
|
mux.HandleFunc(constants.PathChildTraffic, apiHandler.ServeHTTP)
|
||||||
|
mux.HandleFunc(constants.PathChildSpeed, apiHandler.ServeSpeedHTTP)
|
||||||
|
|
||||||
|
// 管理接口
|
||||||
|
mux.HandleFunc(constants.PathChildServiceStats, manageHandler.HandleServicesStatus)
|
||||||
|
mux.HandleFunc(constants.PathChildServiceCtl, manageHandler.HandleServiceControl)
|
||||||
|
mux.HandleFunc(constants.PathChildXrayInstall, manageHandler.HandleXrayInstall)
|
||||||
|
mux.HandleFunc(constants.PathChildXrayRemove, manageHandler.HandleXrayRemove)
|
||||||
|
mux.HandleFunc(constants.PathChildXrayConfig, manageHandler.HandleXrayConfig)
|
||||||
|
mux.HandleFunc(constants.PathChildXraySysCfg, manageHandler.HandleXraySystemConfig)
|
||||||
|
mux.HandleFunc(constants.PathChildXrayCfgFiles, manageHandler.HandleXrayConfigFiles)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxInstall, manageHandler.HandleNginxInstall)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxRemove, manageHandler.HandleNginxRemove)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxConfig, manageHandler.HandleNginxConfig)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxCfgFile, manageHandler.HandleNginxConfigFiles)
|
||||||
|
mux.HandleFunc(constants.PathChildSystemInfo, manageHandler.HandleSystemInfo)
|
||||||
|
mux.HandleFunc(constants.PathChildInbounds, manageHandler.HandleInbounds)
|
||||||
|
mux.HandleFunc(constants.PathChildOutbounds, manageHandler.HandleOutbounds)
|
||||||
|
mux.HandleFunc(constants.PathChildRouting, manageHandler.HandleRouting)
|
||||||
|
mux.HandleFunc(constants.PathChildScan, manageHandler.HandleScan)
|
||||||
|
mux.HandleFunc(constants.PathChildCertDeploy, manageHandler.HandleCertDeploy)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxSetup, manageHandler.HandleNginxSetupSSL)
|
||||||
|
mux.HandleFunc(constants.PathChildDomainProbe, manageHandler.HandleDomainLatencyProbe)
|
||||||
|
|
||||||
|
// SSE 流式安装和卸载接口
|
||||||
|
mux.HandleFunc(constants.PathChildXrayInstallStream, manageHandler.HandleXrayInstallStream)
|
||||||
|
mux.HandleFunc(constants.PathChildXrayRemoveStream, manageHandler.HandleXrayRemoveStream)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxInstallSSE, manageHandler.HandleNginxInstallStream)
|
||||||
|
mux.HandleFunc(constants.PathChildNginxRemoveSSE, manageHandler.HandleNginxRemoveStream)
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clients groups the gRPC stubs that the samples rely on.
|
// Clients 汇总当前用到的 gRPC 客户端。
|
||||||
type Clients struct {
|
type Clients struct {
|
||||||
Connection *grpc.ClientConn
|
Connection *grpc.ClientConn
|
||||||
Handler handlerpb.HandlerServiceClient
|
Handler handlerpb.HandlerServiceClient
|
||||||
@@ -19,7 +19,7 @@ type Clients struct {
|
|||||||
Stats statspb.StatsServiceClient
|
Stats statspb.StatsServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// New establishes an insecure (plaintext) connection against a running Xray API endpoint.
|
// 连接到运行中的 Xray API,默认使用明文连接。
|
||||||
func New(ctx context.Context, addr string, port uint16, dialOpts ...grpc.DialOption) (*Clients, error) {
|
func New(ctx context.Context, addr string, port uint16, dialOpts ...grpc.DialOption) (*Clients, error) {
|
||||||
target := fmt.Sprintf("%s:%d", addr, port)
|
target := fmt.Sprintf("%s:%d", addr, port)
|
||||||
opts := []grpc.DialOption{
|
opts := []grpc.DialOption{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
vmessin "github.com/xtls/xray-core/proxy/vmess/inbound"
|
vmessin "github.com/xtls/xray-core/proxy/vmess/inbound"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddVMessInbound demonstrates HandlerServiceClient.AddInbound for VMess inbound.
|
// 添加 VMess 入站示例。
|
||||||
func AddVMessInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddVMessInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -46,7 +46,7 @@ func AddVMessInbound(ctx context.Context, client command.HandlerServiceClient, t
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddVLESSInbound adds a VLESS inbound with Vision style fallbacks.
|
// 添加带 Vision 回落配置的 VLESS 入站。
|
||||||
func AddVLESSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddVLESSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -80,7 +80,7 @@ func AddVLESSInbound(ctx context.Context, client command.HandlerServiceClient, t
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTrojanInbound registers a Trojan inbound with two users and ALPN fallback.
|
// 添加带双用户和 ALPN 回落的 Trojan 入站。
|
||||||
func AddTrojanInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddTrojanInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -115,7 +115,7 @@ func AddTrojanInbound(ctx context.Context, client command.HandlerServiceClient,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddShadowsocksInbound adds an AEAD Shadowsocks inbound supporting both TCP and UDP.
|
// 添加支持 TCP/UDP 的 AEAD Shadowsocks 入站。
|
||||||
func AddShadowsocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddShadowsocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -137,7 +137,7 @@ func AddShadowsocksInbound(ctx context.Context, client command.HandlerServiceCli
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddShadowsocks2022Inbound covers both single user and multi-user deployment.
|
// 添加 Shadowsocks 2022 入站,兼容单用户和多用户场景。
|
||||||
func AddShadowsocks2022Inbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddShadowsocks2022Inbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
server := &ss2022.MultiUserServerConfig{
|
server := &ss2022.MultiUserServerConfig{
|
||||||
Method: "2022-blake3-aes-128-gcm",
|
Method: "2022-blake3-aes-128-gcm",
|
||||||
@@ -168,7 +168,7 @@ func AddShadowsocks2022Inbound(ctx context.Context, client command.HandlerServic
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddSocksInbound exposes a SOCKS5 server with username/password authentication.
|
// 添加带账号密码认证的 SOCKS5 入站。
|
||||||
func AddSocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddSocksInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -184,7 +184,7 @@ func AddSocksInbound(ctx context.Context, client command.HandlerServiceClient, t
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHTTPInbound adds an HTTP proxy inbound with basic auth.
|
// 添加带基础认证的 HTTP 入站。
|
||||||
func AddHTTPInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddHTTPInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -199,7 +199,7 @@ func AddHTTPInbound(ctx context.Context, client command.HandlerServiceClient, ta
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDokodemoInbound configures a dokodemo-door mirror port.
|
// 添加 dokodemo-door 入站并转发到目标端口。
|
||||||
func AddDokodemoInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetPort uint32) error {
|
func AddDokodemoInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetPort uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -216,7 +216,7 @@ func AddDokodemoInbound(ctx context.Context, client command.HandlerServiceClient
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDNSInbound exposes the built-in DNS server on an API port.
|
// 添加内置 DNS 入站。
|
||||||
func AddDNSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
func AddDNSInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -235,7 +235,7 @@ func AddDNSInbound(ctx context.Context, client command.HandlerServiceClient, tag
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddLoopbackInbound ties an inbound to an existing outbound chain.
|
// 添加 loopback 入站并绑定已有出站链路。
|
||||||
func AddLoopbackInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetInbound string) error {
|
func AddLoopbackInbound(ctx context.Context, client command.HandlerServiceClient, tag string, port uint32, targetInbound string) error {
|
||||||
inbound := inboundConfig(
|
inbound := inboundConfig(
|
||||||
tag,
|
tag,
|
||||||
@@ -245,4 +245,3 @@ func AddLoopbackInbound(ctx context.Context, client command.HandlerServiceClient
|
|||||||
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
_, err := client.AddInbound(ctx, &command.AddInboundRequest{Inbound: inbound})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,4 +195,3 @@ func AddVMessOutbound(ctx context.Context, client command.HandlerServiceClient,
|
|||||||
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
_, err := client.AddOutbound(ctx, &command.AddOutboundRequest{Outbound: cfg})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/proxy/vmess"
|
"github.com/xtls/xray-core/proxy/vmess"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddVMessUser demonstrates AlterInbound(AddUserOperation) for VMess.
|
// 通过 AlterInbound(AddUserOperation) 为 VMess 入站加用户。
|
||||||
func AddVMessUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
func AddVMessUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
@@ -34,7 +34,7 @@ func AddVMessUser(ctx context.Context, client command.HandlerServiceClient, inbo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddVLESSUser shows how to add VLESS users dynamically.
|
// 为 VLESS 入站动态加用户。
|
||||||
func AddVLESSUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
func AddVLESSUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
@@ -53,7 +53,7 @@ func AddVLESSUser(ctx context.Context, client command.HandlerServiceClient, inbo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddTrojanUser adds a Trojan password to an inbound handler.
|
// 为 Trojan 入站加密码用户。
|
||||||
func AddTrojanUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
func AddTrojanUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
@@ -71,7 +71,7 @@ func AddTrojanUser(ctx context.Context, client command.HandlerServiceClient, inb
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddShadowsocksUser sets up a Shadowsocks AEAD credential.
|
// 为 Shadowsocks 入站加 AEAD 凭据。
|
||||||
func AddShadowsocksUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
func AddShadowsocksUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email, password string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
@@ -90,7 +90,7 @@ func AddShadowsocksUser(ctx context.Context, client command.HandlerServiceClient
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddShadowsocks2022User covers key rotation for SS2022.
|
// 为 SS2022 入站新增用户密钥。
|
||||||
func AddShadowsocks2022User(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
func AddShadowsocks2022User(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
@@ -107,7 +107,7 @@ func AddShadowsocks2022User(ctx context.Context, client command.HandlerServiceCl
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUser removes any user (identified by email) from an inbound.
|
// 按邮箱从入站移除用户。
|
||||||
func RemoveUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
func RemoveUser(ctx context.Context, client command.HandlerServiceClient, inboundTag, email string) error {
|
||||||
req := &command.AlterInboundRequest{
|
req := &command.AlterInboundRequest{
|
||||||
Tag: inboundTag,
|
Tag: inboundTag,
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package logger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
|
|
||||||
loggerpb "github.com/xtls/xray-core/app/log/command"
|
loggerpb "github.com/xtls/xray-core/app/log/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RestartLogger triggers the LoggerService restartLogger RPC and waits for completion.
|
// 调用 LoggerService 的 restartLogger 接口并等待完成。
|
||||||
func RestartLogger(ctx context.Context, client loggerpb.LoggerServiceClient) error {
|
func RestartLogger(ctx context.Context, client loggerpb.LoggerServiceClient) error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, constants.DefaultRPCShortTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err := client.RestartLogger(ctx, &loggerpb.RestartLoggerRequest{})
|
_, err := client.RestartLogger(ctx, &loggerpb.RestartLoggerRequest{})
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package stats
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
"mmw-agent/internal/constants"
|
||||||
|
|
||||||
statspb "github.com/xtls/xray-core/app/stats/command"
|
statspb "github.com/xtls/xray-core/app/stats/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func QueryTraffic(ctx context.Context, client statspb.StatsServiceClient, pattern string, reset bool) (int64, error) {
|
func QueryTraffic(ctx context.Context, client statspb.StatsServiceClient, pattern string, reset bool) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, constants.DefaultRPCShortTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
resp, err := client.QueryStats(ctx, &statspb.QueryStatsRequest{
|
resp, err := client.QueryStats(ctx, &statspb.QueryStatsRequest{
|
||||||
Pattern: pattern,
|
Pattern: pattern,
|
||||||
@@ -24,7 +25,7 @@ func QueryTraffic(ctx context.Context, client statspb.StatsServiceClient, patter
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetSystemStats(ctx context.Context, client statspb.StatsServiceClient) (*statspb.SysStatsResponse, error) {
|
func GetSystemStats(ctx context.Context, client statspb.StatsServiceClient) (*statspb.SysStatsResponse, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(ctx, constants.DefaultRPCShortTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return client.GetSysStats(ctx, &statspb.SysStatsRequest{})
|
return client.GetSysStats(ctx, &statspb.SysStatsRequest{})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user