mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-14 19:45:47 +00:00
Merge bc73ce3dde into 169b216d7e
This commit is contained in:
commit
0508bf58f7
1 changed files with 133 additions and 0 deletions
|
|
@ -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{}
|
||||
|
|
|
|||
Loading…
Reference in a new issue