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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -32,6 +33,8 @@ type CheckClientIpJob struct {
|
||||||
|
|
||||||
var job *CheckClientIpJob
|
var job *CheckClientIpJob
|
||||||
|
|
||||||
|
const defaultXrayAPIPort = 62789
|
||||||
|
|
||||||
// NewCheckClientIpJob creates a new client IP monitoring job instance.
|
// NewCheckClientIpJob creates a new client IP monitoring job instance.
|
||||||
func NewCheckClientIpJob() *CheckClientIpJob {
|
func NewCheckClientIpJob() *CheckClientIpJob {
|
||||||
job = new(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)
|
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
|
// Update database with only the currently active (kept) IPs
|
||||||
jsonIps, _ := json.Marshal(keptIps)
|
jsonIps, _ := json.Marshal(keptIps)
|
||||||
inboundClientIps.Ips = string(jsonIps)
|
inboundClientIps.Ips = string(jsonIps)
|
||||||
|
|
@ -378,6 +387,130 @@ func (j *CheckClientIpJob) updateInboundClientIps(inboundClientIps *model.Inboun
|
||||||
return shouldCleanLog
|
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) {
|
func (j *CheckClientIpJob) getInboundByEmail(clientEmail string) (*model.Inbound, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue