mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 14:14:19 +00:00
Fix IP limit fallback Xray API port resolution
This commit is contained in:
parent
9864224897
commit
e468a08a54
2 changed files with 78 additions and 17 deletions
|
|
@ -3,6 +3,7 @@ package job
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -31,6 +32,7 @@ type CheckClientIpJob struct {
|
||||||
disAllowedIps []string
|
disAllowedIps []string
|
||||||
fail2BanWarned bool
|
fail2BanWarned bool
|
||||||
fail2BanInstalled bool
|
fail2BanInstalled bool
|
||||||
|
lastDisconnectOK bool
|
||||||
lastDisconnectByID map[string]int64
|
lastDisconnectByID map[string]int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,6 +341,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||||
|
|
||||||
shouldCleanLog := false
|
shouldCleanLog := false
|
||||||
j.disAllowedIps = []string{}
|
j.disAllowedIps = []string{}
|
||||||
|
j.lastDisconnectOK = false
|
||||||
|
|
||||||
// Open log file
|
// Open log file
|
||||||
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
logIpFile, err := os.OpenFile(xray.GetIPLimitLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
|
@ -367,7 +370,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||||
// Fallback enforcement path when Fail2Ban is unavailable:
|
// Fallback enforcement path when Fail2Ban is unavailable:
|
||||||
// temporarily remove and re-add user to drop existing sessions.
|
// temporarily remove and re-add user to drop existing sessions.
|
||||||
if len(bannedIps) > 0 && !j.fail2BanInstalled {
|
if len(bannedIps) > 0 && !j.fail2BanInstalled {
|
||||||
j.disconnectClientTemporarily(inbound, clientEmail, clients)
|
j.lastDisconnectOK = j.disconnectClientTemporarily(inbound, clientEmail, clients)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update database with only the currently active (kept) IPs
|
// Update database with only the currently active (kept) IPs
|
||||||
|
|
@ -389,8 +392,10 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||||
if len(j.disAllowedIps) > 0 {
|
if len(j.disAllowedIps) > 0 {
|
||||||
if j.fail2BanInstalled {
|
if j.fail2BanInstalled {
|
||||||
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps))
|
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, queued %d new IPs for fail2ban", clientEmail, limitIp, len(j.disAllowedIps))
|
||||||
} else {
|
} else if j.lastDisconnectOK {
|
||||||
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, disconnected session to enforce limit without fail2ban", clientEmail, limitIp)
|
logger.Infof("[LIMIT_IP] Client %s: Kept %d current IPs, disconnected session to enforce limit without fail2ban", clientEmail, limitIp)
|
||||||
|
} else {
|
||||||
|
logger.Warningf("[LIMIT_IP] Client %s: Kept %d current IPs, but failed to disconnect excess sessions without fail2ban", clientEmail, limitIp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,31 +415,27 @@ func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound
|
||||||
}
|
}
|
||||||
|
|
||||||
// disconnectClientTemporarily removes and re-adds a client to force stale/excess sessions to drop.
|
// disconnectClientTemporarily removes and re-adds a client to force stale/excess sessions to drop.
|
||||||
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) {
|
func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, clientEmail string, clients []model.Client) bool {
|
||||||
if inbound == nil || inbound.Tag == "" {
|
if inbound == nil || inbound.Tag == "" {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid thrashing the same account on every 10s cron tick.
|
// Avoid thrashing the same account on every 10s cron tick.
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if last, ok := j.lastDisconnectByID[clientEmail]; ok && now-last < 30 {
|
if last, ok := j.lastDisconnectByID[clientEmail]; ok && now-last < 30 {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var xrayAPI xray.XrayAPI
|
var xrayAPI xray.XrayAPI
|
||||||
db := database.GetDB()
|
apiPort, err := j.resolveXrayAPIPort()
|
||||||
|
if err != nil {
|
||||||
apiPort := 10085
|
logger.Warningf("[LIMIT_IP] Failed to resolve Xray API port for fallback disconnect: %v", err)
|
||||||
var apiPortSetting model.Setting
|
return false
|
||||||
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
|
|
||||||
if parsed, convErr := strconv.Atoi(apiPortSetting.Value); convErr == nil && parsed > 0 {
|
|
||||||
apiPort = parsed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := xrayAPI.Init(apiPort); err != nil {
|
if err := xrayAPI.Init(apiPort); err != nil {
|
||||||
logger.Warningf("[LIMIT_IP] Failed to init Xray API for fallback disconnect: %v", err)
|
logger.Warningf("[LIMIT_IP] Failed to init Xray API for fallback disconnect: %v", err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
defer xrayAPI.Close()
|
defer xrayAPI.Close()
|
||||||
|
|
||||||
|
|
@ -448,19 +449,36 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if clientConfig == nil {
|
if clientConfig == nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := xrayAPI.RemoveUser(inbound.Tag, clientEmail); err != nil {
|
if err := xrayAPI.RemoveUser(inbound.Tag, clientEmail); err != nil {
|
||||||
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
|
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(150 * time.Millisecond)
|
time.Sleep(150 * time.Millisecond)
|
||||||
if err := xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig); err != nil {
|
if err := xrayAPI.AddUser(string(inbound.Protocol), inbound.Tag, clientConfig); err != nil {
|
||||||
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
|
logger.Warningf("[LIMIT_IP] Failed to re-add user %s: %v", clientEmail, err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
j.lastDisconnectByID[clientEmail] = now
|
j.lastDisconnectByID[clientEmail] = now
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *CheckClientIpJob) resolveXrayAPIPort() (int, error) {
|
||||||
|
if apiPort, err := xray.GetAPIPortFromConfig(); err == nil && apiPort > 0 {
|
||||||
|
return apiPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
var apiPortSetting model.Setting
|
||||||
|
if err := db.Where("key = ?", "xrayApiPort").First(&apiPortSetting).Error; err == nil {
|
||||||
|
if parsed, convErr := strconv.Atoi(apiPortSetting.Value); convErr == nil && parsed > 0 {
|
||||||
|
return parsed, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("no usable Xray API port found in config or settings")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,49 @@ func GetAccessLogPath() (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAPIPortFromConfig reads the Xray config and returns the API inbound port.
|
||||||
|
func GetAPIPortFromConfig() (int, error) {
|
||||||
|
config, err := os.ReadFile(GetConfigPath())
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to read configuration file: %s", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonConfig := map[string]any{}
|
||||||
|
if err := json.Unmarshal(config, &jsonConfig); err != nil {
|
||||||
|
logger.Warningf("Failed to parse JSON configuration: %s", err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rawInbounds, ok := jsonConfig["inbounds"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return 0, fmt.Errorf("xray config does not contain inbounds")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rawInbound := range rawInbounds {
|
||||||
|
inbound, ok := rawInbound.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inbound["tag"] != "api" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch port := inbound["port"].(type) {
|
||||||
|
case float64:
|
||||||
|
if port > 0 {
|
||||||
|
return int(port), nil
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
if port > 0 {
|
||||||
|
return port, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, fmt.Errorf("api inbound port not found in xray config")
|
||||||
|
}
|
||||||
|
|
||||||
// stopProcess calls Stop on the given Process instance.
|
// stopProcess calls Stop on the given Process instance.
|
||||||
func stopProcess(p *Process) {
|
func stopProcess(p *Process) {
|
||||||
p.Stop()
|
p.Stop()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue