|
@@ -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{}
|