格式化

This commit is contained in:
iluobei
2026-04-10 15:25:21 +08:00
parent e13c81ce7a
commit b3202edb49
17 changed files with 633 additions and 407 deletions
+20 -50
View File
@@ -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 {
+178 -105
View File
@@ -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,29 +1081,29 @@ 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
@@ -1112,11 +1113,15 @@ func (c *Client) handleDomainLatencyProbe(conn *websocket.Conn, payload WSDomain
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 {
+22 -21
View File
@@ -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
View File
@@ -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
} }
+109
View File
@@ -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",
}
)
+43
View File
@@ -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, "/") {
if _, statErr := os.Stat(candidate); statErr == nil {
nginxPath = candidate
err = nil 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
} }
+41
View File
@@ -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)
}
+2 -2
View File
@@ -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{
+10 -11
View File
@@ -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
} }
+6 -6
View File
@@ -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,
+4 -3
View File
@@ -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
+4 -3
View File
@@ -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{})
} }