mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-13 01:02:46 +00:00
feat: add geo files to nodes and fix func inbounds
This commit is contained in:
parent
b6f336a15c
commit
66662afa4d
4 changed files with 254 additions and 75 deletions
|
|
@ -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:" && \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue