From 66662afa4de9583c372f9f92fdb20ed120958f75 Mon Sep 17 00:00:00 2001 From: Konstantin Pichugin Date: Wed, 7 Jan 2026 22:05:04 +0300 Subject: [PATCH] feat: add geo files to nodes and fix func inbounds --- node/Dockerfile | 4 + node/docker-compose.yml | 38 ------- node/xray/manager.go | 84 ++++++++++++++++ web/controller/inbound.go | 203 +++++++++++++++++++++++++++++++------- 4 files changed, 254 insertions(+), 75 deletions(-) diff --git a/node/Dockerfile b/node/Dockerfile index d6d3f8ba..31a73050 100644 --- a/node/Dockerfile +++ b/node/Dockerfile @@ -65,6 +65,10 @@ RUN mkdir -p bin && \ echo "Downloading geo files..." && \ curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat && \ curl -sfLRO https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat && \ + curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat && \ + curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat && \ + curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat && \ + curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat && \ echo "Final files in bin:" && \ ls -lah && \ echo "File sizes:" && \ diff --git a/node/docker-compose.yml b/node/docker-compose.yml index ea9454dd..d72d6407 100644 --- a/node/docker-compose.yml +++ b/node/docker-compose.yml @@ -18,45 +18,7 @@ services: # If the file doesn't exist, it will be created when XRAY config is first applied networks: - xray-network - node2: - build: - context: .. - dockerfile: node/Dockerfile - container_name: 3x-ui-node2 - restart: unless-stopped - environment: -# - NODE_API_KEY=${NODE_API_KEY:-change-me-to-secure-key} - - NODE_API_KEY=test-key - ports: - - "8081:8080" - - "44001:44000" - volumes: - - ./bin/config.json:/app/bin/config.json - - ./logs:/app/logs - # Note: config.json is mounted directly for persistence - # If the file doesn't exist, it will be created when XRAY config is first applied - networks: - - xray-network - node3: - build: - context: .. - dockerfile: node/Dockerfile - container_name: 3x-ui-node3 - restart: unless-stopped - environment: -# - NODE_API_KEY=${NODE_API_KEY:-change-me-to-secure-key} - - NODE_API_KEY=test-key - ports: - - "8082:8080" - - "44002:44000" - volumes: - - ./bin/config.json:/app/bin/config.json - - ./logs:/app/logs - # Note: config.json is mounted directly for persistence - # If the file doesn't exist, it will be created when XRAY config is first applied - networks: - - xray-network networks: xray-network: driver: bridge diff --git a/node/xray/manager.go b/node/xray/manager.go index 31a92326..a8522275 100644 --- a/node/xray/manager.go +++ b/node/xray/manager.go @@ -5,7 +5,10 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "os" + "path/filepath" "sync" "time" @@ -31,11 +34,92 @@ type Manager struct { // NewManager creates a new XRAY manager instance. func NewManager() *Manager { m := &Manager{} + // Download geo files if missing + m.downloadGeoFiles() // Try to load config from file on startup m.LoadConfigFromFile() return m } +// downloadGeoFiles downloads geo data files if they are missing. +// These files are required for routing rules that use geoip/geosite matching. +func (m *Manager) downloadGeoFiles() { + // Possible bin folder paths (in order of priority) + binPaths := []string{ + "bin", + "/app/bin", + "./bin", + } + + var binPath string + for _, path := range binPaths { + if _, err := os.Stat(path); err == nil { + binPath = path + break + } + } + + if binPath == "" { + logger.Debug("No bin folder found, skipping geo files download") + return + } + + // List of geo files to download + geoFiles := []struct { + URL string + FileName string + }{ + {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"}, + {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"}, + {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"}, + {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"}, + {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"}, + {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"}, + } + + downloadFile := func(url, destPath string) error { + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("failed to download: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("bad status: %d", resp.StatusCode) + } + + file, err := os.Create(destPath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil + } + + for _, file := range geoFiles { + destPath := filepath.Join(binPath, file.FileName) + + // Check if file already exists + if _, err := os.Stat(destPath); err == nil { + logger.Debugf("Geo file %s already exists, skipping download", file.FileName) + continue + } + + logger.Infof("Downloading geo file: %s", file.FileName) + if err := downloadFile(file.URL, destPath); err != nil { + logger.Warningf("Failed to download %s: %v", file.FileName, err) + } else { + logger.Infof("Successfully downloaded %s", file.FileName) + } + } +} + // LoadConfigFromFile attempts to load XRAY configuration from config.json file. // It checks multiple possible locations: bin/config.json, config/config.json, and ./config.json func (m *Manager) LoadConfigFromFile() error { diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 0ba7b7d5..a1b8be40 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -1,8 +1,10 @@ package controller import ( + "bytes" "encoding/json" "fmt" + "io" "strconv" "github.com/mhsanaei/3x-ui/v2/database/model" @@ -104,6 +106,53 @@ func (a *InboundController) getClientTrafficsById(c *gin.Context) { // addInbound creates a new inbound configuration. func (a *InboundController) addInbound(c *gin.Context) { + // Try to get nodeIds from JSON body first (if Content-Type is application/json) + // This must be done BEFORE ShouldBind, which reads the body + var nodeIdsFromJSON []int + var nodeIdFromJSON *int + var hasNodeIdsInJSON, hasNodeIdInJSON bool + + if c.ContentType() == "application/json" { + // Read raw body to extract nodeIds + bodyBytes, err := c.GetRawData() + if err == nil && len(bodyBytes) > 0 { + // Parse JSON to extract nodeIds + var jsonData map[string]interface{} + if err := json.Unmarshal(bodyBytes, &jsonData); err == nil { + // Check for nodeIds array + if nodeIdsVal, ok := jsonData["nodeIds"]; ok { + hasNodeIdsInJSON = true + if nodeIdsArray, ok := nodeIdsVal.([]interface{}); ok { + for _, val := range nodeIdsArray { + if num, ok := val.(float64); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, int(num)) + } else if num, ok := val.(int); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, num) + } + } + } else if num, ok := nodeIdsVal.(float64); ok { + // Single number instead of array + nodeIdsFromJSON = append(nodeIdsFromJSON, int(num)) + } else if num, ok := nodeIdsVal.(int); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, num) + } + } + // Check for nodeId (backward compatibility) + if nodeIdVal, ok := jsonData["nodeId"]; ok { + hasNodeIdInJSON = true + if num, ok := nodeIdVal.(float64); ok { + nodeId := int(num) + nodeIdFromJSON = &nodeId + } else if num, ok := nodeIdVal.(int); ok { + nodeIdFromJSON = &num + } + } + } + // Restore body for ShouldBind + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + } + inbound := &model.Inbound{} err := c.ShouldBind(inbound) if err != nil { @@ -130,19 +179,38 @@ func (a *InboundController) addInbound(c *gin.Context) { // Handle node assignment in multi-node mode nodeService := service.NodeService{} - // Get nodeIds from form (array format: nodeIds=1&nodeIds=2) + // Get nodeIds from form (for form-encoded requests) nodeIdsStr := c.PostFormArray("nodeIds") logger.Debugf("Received nodeIds from form: %v", nodeIdsStr) // Check if nodeIds array was provided (even if empty) nodeIdStr := c.PostForm("nodeId") - if len(nodeIdsStr) > 0 || nodeIdStr != "" { - // Multi-node mode: parse nodeIds array - nodeIds := make([]int, 0) - for _, idStr := range nodeIdsStr { - if idStr != "" { - if id, err := strconv.Atoi(idStr); err == nil && id > 0 { - nodeIds = append(nodeIds, id) + + // Determine which source to use: JSON takes precedence over form data + useJSON := hasNodeIdsInJSON || hasNodeIdInJSON + useForm := (len(nodeIdsStr) > 0 || nodeIdStr != "") && !useJSON + + if useJSON || useForm { + var nodeIds []int + var nodeId *int + + if useJSON { + // Use data from JSON + nodeIds = nodeIdsFromJSON + nodeId = nodeIdFromJSON + } else { + // Parse nodeIds array from form + for _, idStr := range nodeIdsStr { + if idStr != "" { + if id, err := strconv.Atoi(idStr); err == nil && id > 0 { + nodeIds = append(nodeIds, id) + } + } + } + // Parse single nodeId from form + if nodeIdStr != "" && nodeIdStr != "null" { + if parsedId, err := strconv.Atoi(nodeIdStr); err == nil && parsedId > 0 { + nodeId = &parsedId } } } @@ -154,13 +222,10 @@ func (a *InboundController) addInbound(c *gin.Context) { jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) return } - } else if nodeIdStr != "" && nodeIdStr != "null" { + } else if nodeId != nil && *nodeId > 0 { // Backward compatibility: single nodeId - nodeId, err := strconv.Atoi(nodeIdStr) - if err == nil && nodeId > 0 { - if err := nodeService.AssignInboundToNode(inbound.Id, nodeId); err != nil { - logger.Warningf("Failed to assign inbound %d to node %d: %v", inbound.Id, nodeId, err) - } + if err := nodeService.AssignInboundToNode(inbound.Id, *nodeId); err != nil { + logger.Warningf("Failed to assign inbound %d to node %d: %v", inbound.Id, *nodeId, err) } } } @@ -204,8 +269,53 @@ func (a *InboundController) updateInbound(c *gin.Context) { return } - // Get nodeIds from form BEFORE binding to avoid conflict with ShouldBind - // Get nodeIds from form (array format: nodeIds=1&nodeIds=2) + // Try to get nodeIds from JSON body first (if Content-Type is application/json) + var nodeIdsFromJSON []int + var nodeIdFromJSON *int + var hasNodeIdsInJSON, hasNodeIdInJSON bool + + if c.ContentType() == "application/json" { + // Read raw body to extract nodeIds + bodyBytes, err := c.GetRawData() + if err == nil && len(bodyBytes) > 0 { + // Parse JSON to extract nodeIds + var jsonData map[string]interface{} + if err := json.Unmarshal(bodyBytes, &jsonData); err == nil { + // Check for nodeIds array + if nodeIdsVal, ok := jsonData["nodeIds"]; ok { + hasNodeIdsInJSON = true + if nodeIdsArray, ok := nodeIdsVal.([]interface{}); ok { + for _, val := range nodeIdsArray { + if num, ok := val.(float64); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, int(num)) + } else if num, ok := val.(int); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, num) + } + } + } else if num, ok := nodeIdsVal.(float64); ok { + // Single number instead of array + nodeIdsFromJSON = append(nodeIdsFromJSON, int(num)) + } else if num, ok := nodeIdsVal.(int); ok { + nodeIdsFromJSON = append(nodeIdsFromJSON, num) + } + } + // Check for nodeId (backward compatibility) + if nodeIdVal, ok := jsonData["nodeId"]; ok { + hasNodeIdInJSON = true + if num, ok := nodeIdVal.(float64); ok { + nodeId := int(num) + nodeIdFromJSON = &nodeId + } else if num, ok := nodeIdVal.(int); ok { + nodeIdFromJSON = &num + } + } + } + // Restore body for ShouldBind + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + } + + // Get nodeIds from form (for form-encoded requests) nodeIdsStr := c.PostFormArray("nodeIds") logger.Debugf("Received nodeIds from form: %v (count: %d)", nodeIdsStr, len(nodeIdsStr)) @@ -217,6 +327,7 @@ func (a *InboundController) updateInbound(c *gin.Context) { _, hasNodeIds := c.GetPostForm("nodeIds") _, hasNodeId := c.GetPostForm("nodeId") logger.Debugf("Form has nodeIds: %v, has nodeId: %v", hasNodeIds, hasNodeId) + logger.Debugf("JSON has nodeIds: %v (values: %v), has nodeId: %v (value: %v)", hasNodeIdsInJSON, nodeIdsFromJSON, hasNodeIdInJSON, nodeIdFromJSON) inbound := &model.Inbound{ Id: id, @@ -238,20 +349,42 @@ func (a *InboundController) updateInbound(c *gin.Context) { // Handle node assignment in multi-node mode nodeService := service.NodeService{} - if hasNodeIds || hasNodeId { - // Multi-node mode: parse nodeIds array - nodeIds := make([]int, 0) - for _, idStr := range nodeIdsStr { - if idStr != "" { - if id, err := strconv.Atoi(idStr); err == nil && id > 0 { - nodeIds = append(nodeIds, id) - } else { - logger.Warningf("Invalid nodeId in array: %s (error: %v)", idStr, err) + // Determine which source to use: JSON takes precedence over form data + useJSON := hasNodeIdsInJSON || hasNodeIdInJSON + useForm := (hasNodeIds || hasNodeId) && !useJSON + + if useJSON || useForm { + var nodeIds []int + var nodeId *int + var hasNodeIdsFlag bool + + if useJSON { + // Use data from JSON + nodeIds = nodeIdsFromJSON + nodeId = nodeIdFromJSON + hasNodeIdsFlag = hasNodeIdsInJSON + } else { + // Use data from form + hasNodeIdsFlag = hasNodeIds + // Parse nodeIds array from form + for _, idStr := range nodeIdsStr { + if idStr != "" { + if id, err := strconv.Atoi(idStr); err == nil && id > 0 { + nodeIds = append(nodeIds, id) + } else { + logger.Warningf("Invalid nodeId in array: %s (error: %v)", idStr, err) + } + } + } + // Parse single nodeId from form + if nodeIdStr != "" && nodeIdStr != "null" { + if parsedId, err := strconv.Atoi(nodeIdStr); err == nil && parsedId > 0 { + nodeId = &parsedId } } } - logger.Debugf("Parsed nodeIds: %v", nodeIds) + logger.Debugf("Parsed nodeIds: %v, nodeId: %v", nodeIds, nodeId) if len(nodeIds) > 0 { // Assign to multiple nodes @@ -261,19 +394,15 @@ func (a *InboundController) updateInbound(c *gin.Context) { return } logger.Debugf("Successfully assigned inbound %d to nodes %v", inbound.Id, nodeIds) - } else if nodeIdStr != "" && nodeIdStr != "null" { + } else if nodeId != nil && *nodeId > 0 { // Backward compatibility: single nodeId - nodeId, err := strconv.Atoi(nodeIdStr) - if err == nil && nodeId > 0 { - if err := nodeService.AssignInboundToNode(inbound.Id, nodeId); err != nil { - logger.Warningf("Failed to assign inbound %d to node %d: %v", inbound.Id, nodeId, err) - } else { - logger.Debugf("Successfully assigned inbound %d to node %d", inbound.Id, nodeId) - } - } else { - logger.Warningf("Invalid nodeId: %s (error: %v)", nodeIdStr, err) + if err := nodeService.AssignInboundToNode(inbound.Id, *nodeId); err != nil { + logger.Errorf("Failed to assign inbound %d to node %d: %v", inbound.Id, *nodeId, err) + jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) + return } - } else if hasNodeIds { + logger.Debugf("Successfully assigned inbound %d to node %d", inbound.Id, *nodeId) + } else if hasNodeIdsFlag { // nodeIds was explicitly provided but is empty - unassign all if err := nodeService.UnassignInboundFromNode(inbound.Id); err != nil { logger.Warningf("Failed to unassign inbound %d from nodes: %v", inbound.Id, err)