Explorar el Código

revert: Disconnect client due to exceeded IP limit (#3948)

* fix: Ban new IPs with fail2ban  instead of disconnected the client.

* fix: Remove unused strconv import

* fix: Revert log fail2ban format

* fix: Disconnect the client to remove the banned IPs connections

* fix: Fix getting the xray inbound api port

* fix: Run go formatter

* fix: Disconnect only the supported protocols client

* fix: Ensure the required "cipher" field is present  in the shadowsocks protocol

* fix: Log the errors in the resolveXrayAPIPort function

* fix: Run go formatter
HamidReza Sadeghzadeh hace 2 días
padre
commit
1e3b366fba
Se han modificado 1 ficheros con 133 adiciones y 0 borrados
  1. 133 0
      web/job/check_client_ip_job.go

+ 133 - 0
web/job/check_client_ip_job.go

@@ -3,6 +3,7 @@ package job
 import (
 	"bufio"
 	"encoding/json"
+	"errors"
 	"io"
 	"log"
 	"os"
@@ -32,6 +33,8 @@ type CheckClientIpJob struct {
 
 var job *CheckClientIpJob
 
+const defaultXrayAPIPort = 62789
+
 // NewCheckClientIpJob creates a new client IP monitoring job instance.
 func NewCheckClientIpJob() *CheckClientIpJob {
 	job = new(CheckClientIpJob)
@@ -355,6 +358,12 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
 			log.Printf("[LIMIT_IP] Email = %s || Disconnecting OLD IP = %s || Timestamp = %d", clientEmail, ipTime.IP, ipTime.Timestamp)
 		}
 
+		// Actually disconnect banned IPs by temporarily removing and re-adding user
+		// This forces Xray to drop existing connections from banned IPs
+		if len(bannedIps) > 0 {
+			j.disconnectClientTemporarily(inbound, clientEmail, clients)
+		}
+
 		// Update database with only the currently active (kept) IPs
 		jsonIps, _ := json.Marshal(keptIps)
 		inboundClientIps.Ips = string(jsonIps)
@@ -378,6 +387,130 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
 	return shouldCleanLog
 }
 
+// disconnectClientTemporarily removes and re-adds a client to force disconnect banned connections
+func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
+	var xrayAPI xray.XrayAPI
+	apiPort := j.resolveXrayAPIPort()
+
+	err := xrayAPI.Init(apiPort)
+	if err != nil {
+		logger.Warningf("[LIMIT_IP] Failed to init Xray API for disconnection: %v", err)
+		return
+	}
+	defer xrayAPI.Close()
+
+	// Find the client config
+	var clientConfig map[string]any
+	for _, client := range clients {
+		if client.Email == clientEmail {
+			// Convert client to map for API
+			clientBytes, _ := json.Marshal(client)
+			json.Unmarshal(clientBytes, &clientConfig)
+			break
+		}
+	}
+
+	if clientConfig == nil {
+		return
+	}
+
+	// Only perform remove/re-add for protocols supported by XrayAPI.AddUser
+	protocol := string(inbound.Protocol)
+	switch protocol {
+	case "vmess", "vless", "trojan", "shadowsocks":
+		// supported protocols, continue
+	default:
+		logger.Warningf("[LIMIT_IP] Temporary disconnect is not supported for protocol %s on inbound %s", protocol, inbound.Tag)
+		return
+	}
+
+	// For Shadowsocks, ensure the required "cipher" field is present by
+	// reading it from the inbound settings (e.g., settings["method"]).
+	if string(inbound.Protocol) == "shadowsocks" {
+		var inboundSettings map[string]any
+		if err := json.Unmarshal([]byte(inbound.Settings), &inboundSettings); err != nil {
+			logger.Warningf("[LIMIT_IP] Failed to parse inbound settings for shadowsocks cipher: %v", err)
+		} else {
+			if method, ok := inboundSettings["method"].(string); ok && method != "" {
+				clientConfig["cipher"] = method
+			}
+		}
+	}
+
+	// Remove user to disconnect all connections
+	err = xrayAPI.RemoveUser(inbound.Tag, clientEmail)
+	if err != nil {
+		logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
+		return
+	}
+
+	// Wait a moment for disconnection to take effect
+	time.Sleep(100 * time.Millisecond)
+
+	// Re-add user to allow new connections
+	err = xrayAPI.AddUser(protocol, inbound.Tag, clientConfig)
+	if err != nil {
+		logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
+	}
+}
+
+// resolveXrayAPIPort returns the API inbound port from running config, then template config, then default.
+func (j *CheckClientIpJob) resolveXrayAPIPort() int {
+	var configErr error
+	var templateErr error
+
+	if port, err := getAPIPortFromConfigPath(xray.GetConfigPath()); err == nil {
+		return port
+	} else {
+		configErr = err
+	}
+
+	db := database.GetDB()
+	var template model.Setting
+	if err := db.Where("key = ?", "xrayTemplateConfig").First(&template).Error; err == nil {
+		if port, parseErr := getAPIPortFromConfigData([]byte(template.Value)); parseErr == nil {
+			return port
+		} else {
+			templateErr = parseErr
+		}
+	} else {
+		templateErr = err
+	}
+
+	logger.Warningf(
+		"[LIMIT_IP] Could not determine Xray API port from config or template; falling back to default port %d (config error: %v, template error: %v)",
+		defaultXrayAPIPort,
+		configErr,
+		templateErr,
+	)
+
+	return defaultXrayAPIPort
+}
+
+func getAPIPortFromConfigPath(configPath string) (int, error) {
+	configData, err := os.ReadFile(configPath)
+	if err != nil {
+		return 0, err
+	}
+
+	return getAPIPortFromConfigData(configData)
+}
+
+func getAPIPortFromConfigData(configData []byte) (int, error) {
+	xrayConfig := &xray.Config{}
+	if err := json.Unmarshal(configData, xrayConfig); err != nil {
+		return 0, err
+	}
+
+	for _, inboundConfig := range xrayConfig.InboundConfigs {
+		if inboundConfig.Tag == "api" && inboundConfig.Port > 0 {
+			return inboundConfig.Port, nil
+		}
+	}
+
+	return 0, errors.New("api inbound port not found")
+}
+
 func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
 	db := database.GetDB()
 	inbound := &model.Inbound{}