Files
3x-ui/internal/web/service/tgbot/tgbot_inbound.go
T
BlindMaster24 87420e3bb1 fix(tgbot): close stale-inbound TOCTOU and contain handler panics (#6442)
Tapping an old get_clients_for_* inline keyboard re-fetched the inbound
after the keyboard lookup, discarding the error; if the row vanished
between the two reads, the second GetInbound returned nil and
inbound.Remark panicked. The callback handler runs on a bare goroutine
with no recover(), so that panic killed the whole panel process.

Fetch the inbound once in a shared chooseInboundClient helper that
answers an error callback on a missing row, and pass the row down to
getInboundClientsFor instead of re-reading the DB, removing the
between-reads window. Route all three OnReceive handler paths through a
recover() barrier so no handler panic can take down the process, and log
the GetInbound failure instead of silently swallowing it.
2026-09-10 09:41:52 +02:00

302 lines
9.7 KiB
Go

package tgbot
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
"github.com/mymmrac/telego"
tu "github.com/mymmrac/telego/telegoutil"
)
// getInboundUsages retrieves and formats inbound usage information.
func (t *Tgbot) getInboundUsages() string {
var info strings.Builder
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
return info.String()
}
for _, inbound := range inbounds {
info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark))
info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)))
info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
clients, listErr := t.clientService.ListForInbound(nil, inbound.Id)
if listErr == nil {
fmt.Fprintf(&info, "👥 Clients: %d\r\n", len(clients))
}
if inbound.ExpiryTime == 0 {
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
} else {
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
}
info.WriteString("\r\n")
}
return info.String()
}
// getInbounds creates an inline keyboard with all inbounds.
func (t *Tgbot) getInbounds() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
if len(inbounds) == 0 {
logger.Warning("No inbounds found")
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
var buttons []telego.InlineKeyboardButton
for _, inbound := range inbounds {
status := "❌"
if inbound.Enable {
status = "✅"
}
callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "get_clients", inbound.Id))
buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
return keyboard, nil
}
// getInboundsFor builds an inline keyboard of inbounds for a custom next action.
func (t *Tgbot) getInboundsFor(nextAction string) (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
if len(inbounds) == 0 {
logger.Warning("No inbounds found")
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
var buttons []telego.InlineKeyboardButton
for _, inbound := range inbounds {
status := "❌"
if inbound.Enable {
status = "✅"
}
callbackData := t.encodeQuery(fmt.Sprintf("%s %d", nextAction, inbound.Id))
buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
return keyboard, nil
}
// getInboundClientsFor lists clients of an inbound with a specific action prefix to be appended with email
func (t *Tgbot) getInboundClientsFor(inbound *model.Inbound, action string) (*telego.InlineKeyboardMarkup, error) {
clients, err := t.inboundService.GetClients(inbound)
var buttons []telego.InlineKeyboardButton
if err != nil {
logger.Warning("GetInboundClients run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
} else {
if len(clients) > 0 {
for _, client := range clients {
buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery(action+" "+client.Email)))
}
} else {
return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed"))
}
}
cols := 0
if len(buttons) < 6 {
cols = 3
} else {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
return keyboard, nil
}
// getInboundsAddClient creates an inline keyboard for adding clients to inbounds.
func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
if len(inbounds) == 0 {
logger.Warning("No inbounds found")
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
excludedProtocols := map[model.Protocol]bool{
model.Tunnel: true,
model.Mixed: true,
model.WireGuard: true,
model.AmneziaWG: true,
model.HTTP: true,
}
var buttons []telego.InlineKeyboardButton
for _, inbound := range inbounds {
if excludedProtocols[inbound.Protocol] {
continue
}
status := "❌"
if inbound.Enable {
status = "✅"
}
callbackData := t.encodeQuery(fmt.Sprintf("%s %d", "add_client_to", inbound.Id))
buttons = append(buttons, tu.InlineKeyboardButton(fmt.Sprintf("%v - %v", inbound.Remark, status)).WithCallbackData(callbackData))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
return keyboard, nil
}
// getInboundsAttachPicker builds a toggle picker over multi-client inbounds
// for the "attach more inbounds to the new client" step. Each row shows the
// current selection state for the inbound; tapping fires
// add_client_toggle_attach <id> which flips it and re-renders. A final
// "Done" button (add_client_attach_done) returns to the field-edit screen.
func (t *Tgbot) getInboundsAttachPicker() (*telego.InlineKeyboardMarkup, error) {
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
if len(inbounds) == 0 {
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
excludedProtocols := map[model.Protocol]bool{
model.Tunnel: true,
model.Mixed: true,
model.WireGuard: true,
model.AmneziaWG: true,
model.HTTP: true,
}
selected := make(map[int]bool, len(receiver_inbound_IDs))
for _, id := range receiver_inbound_IDs {
selected[id] = true
}
var buttons []telego.InlineKeyboardButton
for _, ib := range inbounds {
if excludedProtocols[ib.Protocol] {
continue
}
mark := "☐"
if selected[ib.Id] {
mark = "✅"
}
label := fmt.Sprintf("%s %s (%s)", mark, ib.Remark, ib.Protocol)
callback := t.encodeQuery(fmt.Sprintf("add_client_toggle_attach %d", ib.Id))
buttons = append(buttons, tu.InlineKeyboardButton(label).WithCallbackData(callback))
}
cols := 1
if len(buttons) >= 6 {
cols = 2
}
rows := tu.InlineKeyboardCols(cols, buttons...)
rows = append(rows, tu.InlineKeyboardRow(
tu.InlineKeyboardButton("✅ Done").WithCallbackData(t.encodeQuery("add_client_attach_done")),
))
return tu.InlineKeyboardGrid(rows), nil
}
// getInboundClients creates an inline keyboard with clients of a specific inbound.
func (t *Tgbot) getInboundClients(id int) (*telego.InlineKeyboardMarkup, error) {
inbound, err := t.inboundService.GetInbound(id)
if err != nil {
logger.Warning("getIboundClients run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
}
clients, err := t.inboundService.GetClients(inbound)
var buttons []telego.InlineKeyboardButton
if err != nil {
logger.Warning("GetInboundClients run failed:", err)
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
} else {
if len(clients) > 0 {
for _, client := range clients {
buttons = append(buttons, tu.InlineKeyboardButton(client.Email).WithCallbackData(t.encodeQuery("client_get_usage "+client.Email)))
}
} else {
return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed"))
}
}
cols := 0
if len(buttons) < 6 {
cols = 3
} else {
cols = 2
}
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
return keyboard, nil
}
// searchInbound searches for inbounds by remark and sends the results.
func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbounds, err := t.inboundService.SearchInbounds(remark)
if err != nil {
logger.Warning(err)
msg := t.I18nBot("tgbot.wentWrong")
t.SendMsgToTgbot(chatId, msg)
return
}
if len(inbounds) == 0 {
msg := t.I18nBot("tgbot.noInbounds")
t.SendMsgToTgbot(chatId, msg)
return
}
for _, inbound := range inbounds {
info := ""
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
} else {
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
t.SendMsgToTgbot(chatId, info)
if len(inbound.ClientStats) > 0 {
var output strings.Builder
for _, traffic := range inbound.ClientStats {
output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true))
}
t.SendMsgToTgbot(chatId, output.String())
}
}
}