Fix IP limit fallback Xray API port resolution

This commit is contained in:
Sora39831 2026-04-06 16:31:39 +08:00
parent 9864224897
commit e468a08a54
2 changed files with 78 additions and 17 deletions

View file

@ -3,6 +3,7 @@ package job
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"os"
@ -31,6 +32,7 @@ type CheckClientIpJob struct {
disAllowedIps []string
fail2BanWarned bool
fail2BanInstalled bool
lastDisconnectOK bool
lastDisconnectByID map[string]int64
}
@ -339,6 +341,7 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
shouldCleanLog := false
j.disAllowedIps = []string{}
j.lastDisconnectOK = false
// Open log file
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:
// temporarily remove and re-add user to drop existing sessions.
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
@ -389,8 +392,10 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
if len(j.disAllowedIps) > 0 {
if j.fail2BanInstalled {
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)
} 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.
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 == "" {
return
return false
}
// Avoid thrashing the same account on every 10s cron tick.
now := time.Now().Unix()
if last, ok := j.lastDisconnectByID[clientEmail]; ok && now-last < 30 {
return
return true
}
var xrayAPI xray.XrayAPI
db := database.GetDB()
apiPort := 10085
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 {
apiPort = parsed
}
apiPort, err := j.resolveXrayAPIPort()
if err != nil {
logger.Warningf("[LIMIT_IP] Failed to resolve Xray API port for fallback disconnect: %v", err)
return false
}
if err := xrayAPI.Init(apiPort); err != nil {
logger.Warningf("[LIMIT_IP] Failed to init Xray API for fallback disconnect: %v", err)
return
return false
}
defer xrayAPI.Close()
@ -448,19 +449,36 @@ func (j *CheckClientIpJob) disconnectClientTemporarily(inbound *model.Inbound, c
break
}
if clientConfig == nil {
return
return false
}
if err := xrayAPI.RemoveUser(inbound.Tag, clientEmail); err != nil {
logger.Warningf("[LIMIT_IP] Failed to remove user %s: %v", clientEmail, err)
return
return false
}
time.Sleep(150 * time.Millisecond)
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)
return
return false
}
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")
}

View file

@ -93,6 +93,49 @@ func GetAccessLogPath() (string, error) {
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.
func stopProcess(p *Process) {
p.Stop()