mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-01-15 10:09:34 +00:00
Compare commits
10 commits
bd6d16de0c
...
ba59b470b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba59b470b7 | ||
|
|
278aa1c85c | ||
|
|
8fe297ef9d | ||
|
|
c881d1015a | ||
|
|
c061337ce7 | ||
|
|
94b9abca62 | ||
|
|
2a99726b6c | ||
|
|
cfccaa259c | ||
|
|
420b4e49c9 | ||
|
|
f5eab61903 |
10 changed files with 123 additions and 49 deletions
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -17,7 +17,8 @@ on:
|
||||||
- '**.go'
|
- '**.go'
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
- 'x-ui.service'
|
- 'x-ui.service.debian'
|
||||||
|
- 'x-ui.service.rhel'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
@ -78,7 +79,8 @@ jobs:
|
||||||
|
|
||||||
mkdir x-ui
|
mkdir x-ui
|
||||||
cp xui-release x-ui/
|
cp xui-release x-ui/
|
||||||
cp x-ui.service x-ui/
|
cp x-ui.service.debian x-ui/
|
||||||
|
cp x-ui.service.rhel x-ui/
|
||||||
cp x-ui.sh x-ui/
|
cp x-ui.sh x-ui/
|
||||||
mv x-ui/xui-release x-ui/x-ui
|
mv x-ui/xui-release x-ui/x-ui
|
||||||
mkdir x-ui/bin
|
mkdir x-ui/bin
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ func GetLogFolder() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return filepath.Join(".", "log")
|
return filepath.Join(".", "log")
|
||||||
}
|
}
|
||||||
return "/var/log"
|
return "/var/log/x-ui"
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
|
|
|
||||||
14
install.sh
14
install.sh
|
|
@ -641,6 +641,7 @@ install_x-ui() {
|
||||||
# Update x-ui cli and se set permission
|
# Update x-ui cli and se set permission
|
||||||
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
||||||
chmod +x /usr/bin/x-ui
|
chmod +x /usr/bin/x-ui
|
||||||
|
mkdir -p /var/log/x-ui
|
||||||
config_after_install
|
config_after_install
|
||||||
|
|
||||||
if [[ $release == "alpine" ]]; then
|
if [[ $release == "alpine" ]]; then
|
||||||
|
|
@ -653,7 +654,18 @@ install_x-ui() {
|
||||||
rc-update add x-ui
|
rc-update add x-ui
|
||||||
rc-service x-ui start
|
rc-service x-ui start
|
||||||
else
|
else
|
||||||
cp -f x-ui.service /etc/systemd/system/
|
if [ -f "x-ui.service" ]; then
|
||||||
|
cp -f x-ui.service /etc/systemd/system/
|
||||||
|
else
|
||||||
|
case "${release}" in
|
||||||
|
ubuntu | debian | armbian)
|
||||||
|
cp -f x-ui.service.debian /etc/systemd/system/x-ui.service
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
cp -f x-ui.service.rhel /etc/systemd/system/x-ui.service
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable x-ui
|
systemctl enable x-ui
|
||||||
systemctl start x-ui
|
systemctl start x-ui
|
||||||
|
|
|
||||||
20
update.sh
20
update.sh
|
|
@ -615,6 +615,8 @@ update_x-ui() {
|
||||||
echo -e "${green}Removing old x-ui version...${plain}"
|
echo -e "${green}Removing old x-ui version...${plain}"
|
||||||
rm /usr/bin/x-ui -f >/dev/null 2>&1
|
rm /usr/bin/x-ui -f >/dev/null 2>&1
|
||||||
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
|
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
|
||||||
|
rm /usr/local/x-ui/x-ui.service.debain -f >/dev/null 2>&1
|
||||||
|
rm /usr/local/x-ui/x-ui.service.rhel -f >/dev/null 2>&1
|
||||||
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
|
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
|
||||||
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
|
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
|
||||||
echo -e "${green}Removing old xray version...${plain}"
|
echo -e "${green}Removing old xray version...${plain}"
|
||||||
|
|
@ -653,6 +655,7 @@ update_x-ui() {
|
||||||
|
|
||||||
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
||||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||||
|
mkdir -p /var/log/x-ui >/dev/null 2>&1
|
||||||
|
|
||||||
echo -e "${green}Changing owner...${plain}"
|
echo -e "${green}Changing owner...${plain}"
|
||||||
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
||||||
|
|
@ -676,8 +679,21 @@ update_x-ui() {
|
||||||
rc-update add x-ui >/dev/null 2>&1
|
rc-update add x-ui >/dev/null 2>&1
|
||||||
rc-service x-ui start >/dev/null 2>&1
|
rc-service x-ui start >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
echo -e "${green}Installing systemd unit...${plain}"
|
if [ -f "x-ui.service" ]; then
|
||||||
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
|
echo -e "${green}Installing systemd unit...${plain}"
|
||||||
|
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
case "${release}" in
|
||||||
|
ubuntu | debian | armbian)
|
||||||
|
echo -e "${green}Installing debian-like systemd unit...${plain}"
|
||||||
|
cp -f x-ui.service.debian /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${green}Installing rhel-like systemd unit...${plain}"
|
||||||
|
cp -f x-ui.service.rhel /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
||||||
systemctl daemon-reload >/dev/null 2>&1
|
systemctl daemon-reload >/dev/null 2>&1
|
||||||
systemctl enable x-ui >/dev/null 2>&1
|
systemctl enable x-ui >/dev/null 2>&1
|
||||||
|
|
|
||||||
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -529,6 +529,18 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Check HTTP status code - GitHub API returns object instead of array on error
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
var errorResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
|
||||||
|
return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
buffer := bytes.NewBuffer(make([]byte, bufferSize))
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
if _, err := buffer.ReadFrom(resp.Body); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,10 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If Start is called again (e.g. during reload), ensure any previous long-polling
|
||||||
|
// loop is stopped before creating a new bot / receiver.
|
||||||
|
StopBot()
|
||||||
|
|
||||||
// Initialize hash storage to store callback queries
|
// Initialize hash storage to store callback queries
|
||||||
hashStorage = global.NewHashStorage(20 * time.Minute)
|
hashStorage = global.NewHashStorage(20 * time.Minute)
|
||||||
|
|
||||||
|
|
@ -207,6 +211,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsedAdminIds := make([]int64, 0)
|
||||||
// Parse admin IDs from comma-separated string
|
// Parse admin IDs from comma-separated string
|
||||||
if tgBotID != "" {
|
if tgBotID != "" {
|
||||||
for _, adminID := range strings.Split(tgBotID, ",") {
|
for _, adminID := range strings.Split(tgBotID, ",") {
|
||||||
|
|
@ -215,9 +220,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
adminIds = append(adminIds, int64(id))
|
parsedAdminIds = append(parsedAdminIds, int64(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
adminIds = parsedAdminIds
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get Telegram bot proxy URL
|
// Get Telegram bot proxy URL
|
||||||
tgBotProxy, err := t.settingService.GetTgBotProxy()
|
tgBotProxy, err := t.settingService.GetTgBotProxy()
|
||||||
|
|
@ -252,10 +260,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start receiving Telegram bot messages
|
// Start receiving Telegram bot messages
|
||||||
if !isRunning {
|
tgBotMutex.Lock()
|
||||||
|
alreadyRunning := isRunning || botCancel != nil
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
if !alreadyRunning {
|
||||||
logger.Info("Telegram bot receiver started")
|
logger.Info("Telegram bot receiver started")
|
||||||
go t.OnReceive()
|
go t.OnReceive()
|
||||||
isRunning = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -300,6 +310,8 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel
|
||||||
|
|
||||||
// IsRunning checks if the Telegram bot is currently running.
|
// IsRunning checks if the Telegram bot is currently running.
|
||||||
func (t *Tgbot) IsRunning() bool {
|
func (t *Tgbot) IsRunning() bool {
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
defer tgBotMutex.Unlock()
|
||||||
return isRunning
|
return isRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -317,34 +329,34 @@ func (t *Tgbot) SetHostname() {
|
||||||
// Stop safely stops the Telegram bot's Long Polling operation.
|
// Stop safely stops the Telegram bot's Long Polling operation.
|
||||||
// This method now calls the global StopBot function and cleans up other resources.
|
// This method now calls the global StopBot function and cleans up other resources.
|
||||||
func (t *Tgbot) Stop() {
|
func (t *Tgbot) Stop() {
|
||||||
// Call the global StopBot function to gracefully shut down Long Polling
|
|
||||||
StopBot()
|
StopBot()
|
||||||
|
|
||||||
// Stop the bot handler (in case the goroutine hasn't exited yet)
|
|
||||||
if botHandler != nil {
|
|
||||||
botHandler.Stop()
|
|
||||||
}
|
|
||||||
logger.Info("Stop Telegram receiver ...")
|
logger.Info("Stop Telegram receiver ...")
|
||||||
isRunning = false
|
tgBotMutex.Lock()
|
||||||
adminIds = nil
|
adminIds = nil
|
||||||
|
tgBotMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
|
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
|
||||||
// This is the global function called from main.go's signal handler and t.Stop().
|
// This is the global function called from main.go's signal handler and t.Stop().
|
||||||
func StopBot() {
|
func StopBot() {
|
||||||
|
// Don't hold the mutex while cancelling/waiting.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
defer tgBotMutex.Unlock()
|
cancel := botCancel
|
||||||
|
botCancel = nil
|
||||||
|
handler := botHandler
|
||||||
|
botHandler = nil
|
||||||
|
isRunning = false
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
if botCancel != nil {
|
if handler != nil {
|
||||||
|
handler.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if cancel != nil {
|
||||||
logger.Info("Sending cancellation signal to Telegram bot...")
|
logger.Info("Sending cancellation signal to Telegram bot...")
|
||||||
|
// Cancels the context passed to UpdatesViaLongPolling; this closes updates channel
|
||||||
// Calling botCancel() cancels the context passed to UpdatesViaLongPolling,
|
// and lets botHandler.Start() exit cleanly.
|
||||||
// which stops the Long Polling operation and closes the updates channel,
|
cancel()
|
||||||
// allowing the th.Start() goroutine to exit cleanly.
|
|
||||||
botCancel()
|
|
||||||
|
|
||||||
botCancel = nil
|
|
||||||
// Giving the goroutine a small delay to exit cleanly.
|
|
||||||
botWG.Wait()
|
botWG.Wait()
|
||||||
logger.Info("Telegram bot successfully stopped.")
|
logger.Info("Telegram bot successfully stopped.")
|
||||||
}
|
}
|
||||||
|
|
@ -379,36 +391,38 @@ func (t *Tgbot) OnReceive() {
|
||||||
params := telego.GetUpdatesParams{
|
params := telego.GetUpdatesParams{
|
||||||
Timeout: 30, // Increased timeout to reduce API calls
|
Timeout: 30, // Increased timeout to reduce API calls
|
||||||
}
|
}
|
||||||
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
|
// Strict singleton: never start a second long-polling loop.
|
||||||
tgBotMutex.Lock()
|
tgBotMutex.Lock()
|
||||||
|
if botCancel != nil || isRunning {
|
||||||
// Create a context with cancellation and store the cancel function.
|
tgBotMutex.Unlock()
|
||||||
var ctx context.Context
|
logger.Warning("TgBot OnReceive called while already running; ignoring.")
|
||||||
|
return
|
||||||
// Check if botCancel is already set (to prevent race condition overwrite and goroutine leak)
|
|
||||||
if botCancel == nil {
|
|
||||||
ctx, botCancel = context.WithCancel(context.Background())
|
|
||||||
} else {
|
|
||||||
// If botCancel is already set, use a non-cancellable context for this redundant call.
|
|
||||||
// This prevents overwriting the active botCancel and causing a goroutine leak from the previous call.
|
|
||||||
logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.")
|
|
||||||
ctx = context.Background() // <<< ИЗМЕНЕНИЕ
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
botCancel = cancel
|
||||||
|
isRunning = true
|
||||||
|
// Add to WaitGroup before releasing the lock so StopBot() can't return
|
||||||
|
// before this receiver goroutine is accounted for.
|
||||||
|
botWG.Add(1)
|
||||||
tgBotMutex.Unlock()
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
// Get updates channel using the context.
|
// Get updates channel using the context.
|
||||||
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
||||||
botWG.Go(func() {
|
go func() {
|
||||||
|
defer botWG.Done()
|
||||||
|
h, _ := th.NewBotHandler(bot, updates)
|
||||||
|
tgBotMutex.Lock()
|
||||||
|
botHandler = h
|
||||||
|
tgBotMutex.Unlock()
|
||||||
|
|
||||||
botHandler, _ = th.NewBotHandler(bot, updates)
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
|
||||||
delete(userStates, message.Chat.ID)
|
delete(userStates, message.Chat.ID)
|
||||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
||||||
return nil
|
return nil
|
||||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
// Use goroutine with worker pool for concurrent command processing
|
// Use goroutine with worker pool for concurrent command processing
|
||||||
go func() {
|
go func() {
|
||||||
messageWorkerPool <- struct{}{} // Acquire worker
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
|
@ -420,7 +434,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCommand())
|
}, th.AnyCommand())
|
||||||
|
|
||||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||||
// Use goroutine with worker pool for concurrent callback processing
|
// Use goroutine with worker pool for concurrent callback processing
|
||||||
go func() {
|
go func() {
|
||||||
messageWorkerPool <- struct{}{} // Acquire worker
|
messageWorkerPool <- struct{}{} // Acquire worker
|
||||||
|
|
@ -432,7 +446,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyCallbackQueryWithMessage())
|
}, th.AnyCallbackQueryWithMessage())
|
||||||
|
|
||||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
h.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||||
if userState, exists := userStates[message.Chat.ID]; exists {
|
if userState, exists := userStates[message.Chat.ID]; exists {
|
||||||
switch userState {
|
switch userState {
|
||||||
case "awaiting_id":
|
case "awaiting_id":
|
||||||
|
|
@ -578,8 +592,8 @@ func (t *Tgbot) OnReceive() {
|
||||||
return nil
|
return nil
|
||||||
}, th.AnyMessage())
|
}, th.AnyMessage())
|
||||||
|
|
||||||
botHandler.Start()
|
h.Start()
|
||||||
})
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// answerCommand processes incoming command messages from Telegram users.
|
// answerCommand processes incoming command messages from Telegram users.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ After=network.target
|
||||||
Wants=network.target
|
Wants=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
EnvironmentFile=-/etc/default/x-ui
|
||||||
Environment="XRAY_VMESS_AEAD_FORCED=false"
|
Environment="XRAY_VMESS_AEAD_FORCED=false"
|
||||||
Type=simple
|
Type=simple
|
||||||
WorkingDirectory=/usr/local/x-ui/
|
WorkingDirectory=/usr/local/x-ui/
|
||||||
16
x-ui.service.rhel
Normal file
16
x-ui.service.rhel
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[Unit]
|
||||||
|
Description=x-ui Service
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=-/etc/sysconfig/x-ui
|
||||||
|
Environment="XRAY_VMESS_AEAD_FORCED=false"
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/usr/local/x-ui/
|
||||||
|
ExecStart=/usr/local/x-ui/x-ui
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
3
x-ui.sh
3
x-ui.sh
|
|
@ -53,7 +53,8 @@ os_version=""
|
||||||
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.')
|
||||||
|
|
||||||
# Declare Variables
|
# Declare Variables
|
||||||
log_folder="${XUI_LOG_FOLDER:=/var/log}"
|
log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}"
|
||||||
|
mkdir -p "${log_folder}"
|
||||||
iplimit_log_path="${log_folder}/3xipl.log"
|
iplimit_log_path="${log_folder}/3xipl.log"
|
||||||
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
|
iplimit_banned_log_path="${log_folder}/3xipl-banned.log"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue