Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorHamidReza Sadeghzadeh <HamidrezaSZ@proton.me>2026-04-19 22:52:40 +0300
committerGitHub <noreply@github.com>2026-04-19 22:52:40 +0300
commit1e3b366fba3054698d55e36891d022a513e5a942 (patch)
treef794b69d915286698869c802a2b34f051247475b /web
parentc2a2a36f5685bd90ce1a9eb8b1b09c5dd24bf6c2 (diff)
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
Diffstat (limited to 'web')
-rw-r--r--web/job/check_client_ip_job.go133
1 files changed, 133 insertions, 0 deletions
diff --git a/web/job/check_client_ip_job.go b/web/job/check_client_ip_job.go
index cbc352dc..312a8eee 100644
--- a/web/job/check_client_ip_job.go
+++ b/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{}