feat: add geo files to nodes and fix func inbounds

This commit is contained in:
Konstantin Pichugin 2026-01-07 22:05:04 +03:00
parent b6f336a15c
commit 66662afa4d
4 changed files with 254 additions and 75 deletions

View file

@ -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:" && \

View file

@ -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

View file

@ -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 {

View file

@ -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)