3x-ui/web/service/fallback.go

148 lines
3.7 KiB
Go
Raw Normal View History

feat(inbounds): native fallbacks on VLESS/Trojan TCP-TLS, with working child links A VLESS or Trojan inbound on TCP with TLS or Reality can now act as a fallback master: pick existing inbounds as children and the panel auto- fills the SNI / ALPN / path / xver routing fields from each child's transport, auto-builds settings.fallbacks at config-gen time, and rewrites the child's client-share link so it advertises the master's reachable endpoint and TLS state instead of the child's loopback listen. Layout matches the Xray All-in-One Nginx example: master at :443 with clients + TLS, each child on 127.0.0.1 with its own transport+clients. Order matters (Xray walks fallbacks top-to-bottom) — reorder via the per-row up/down arrows. Path / SNI / ALPN are exposed under a per-row Edit toggle for the rare cases where the auto-derivation needs overriding; otherwise just pick a child and you're done. Backend: new InboundFallback table + FallbackService (GetByMaster / SetByMaster / GetParentForChild / BuildFallbacksJSON); two routes (GET / POST /panel/api/inbounds/:id/fallbacks); xray.GetXrayConfig injects settings.fallbacks for any VLESS/Trojan TCP-TLS/Reality inbound; GetInbounds annotates each child with FallbackParent so the frontend can rewrite links without an extra round-trip. Link projection covers every emission path — clients-page QR/links, per-inbound Get URL, raw subscription, sub-JSON, sub-Clash, and the inbounds-page link/info/QR — via a shared projectThroughFallbackMaster on the backend and a shared projectChildThroughMaster on the frontend that both handle the panel-tracked relationship and the legacy unix-socket (@vless-ws) convention. Strings translated into all 12 non-English locales.
2026-05-18 23:11:09 +00:00
package service
import (
"fmt"
"strings"
"github.com/mhsanaei/3x-ui/v3/database"
"github.com/mhsanaei/3x-ui/v3/database/model"
"gorm.io/gorm"
)
type FallbackService struct{}
// FallbackInput is the payload shape POSTed by the inbound form.
type FallbackInput struct {
ChildId int `json:"childId"`
Name string `json:"name"`
Alpn string `json:"alpn"`
Path string `json:"path"`
Xver int `json:"xver"`
SortOrder int `json:"sortOrder"`
}
// GetByMaster returns every fallback rule attached to the master inbound.
func (s *FallbackService) GetByMaster(masterId int) ([]model.InboundFallback, error) {
var rows []model.InboundFallback
err := database.GetDB().
Where("master_id = ?", masterId).
Order("sort_order ASC, id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
return rows, nil
}
// GetParentForChild finds the first fallback rule that points at childId.
// Used by client-link generation: when a child inbound is attached as a
// fallback, its client links should advertise the master's address+port
// and TLS instead of the child's loopback listen.
func (s *FallbackService) GetParentForChild(childId int) (*model.InboundFallback, error) {
var row model.InboundFallback
err := database.GetDB().
Where("child_id = ?", childId).
Order("sort_order ASC, id ASC").
First(&row).Error
if err == gorm.ErrRecordNotFound {
return nil, nil
}
if err != nil {
return nil, err
}
return &row, nil
}
// SetByMaster replaces the master's entire fallback list atomically.
func (s *FallbackService) SetByMaster(masterId int, items []FallbackInput) error {
db := database.GetDB()
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("master_id = ?", masterId).Delete(&model.InboundFallback{}).Error; err != nil {
return err
}
for i, c := range items {
if c.ChildId <= 0 || c.ChildId == masterId {
continue
}
row := model.InboundFallback{
MasterId: masterId,
ChildId: c.ChildId,
Name: c.Name,
Alpn: c.Alpn,
Path: c.Path,
Xver: c.Xver,
SortOrder: c.SortOrder,
}
if row.SortOrder == 0 {
row.SortOrder = i
}
if err := tx.Create(&row).Error; err != nil {
return err
}
}
return nil
})
}
// BuildFallbacksJSON resolves the master's fallback rows into Xray's
// expected settings.fallbacks shape, looking up each child's listen+port
// to fill the dest field. Returns nil when the master has no rules.
func (s *FallbackService) BuildFallbacksJSON(tx *gorm.DB, masterId int) ([]map[string]any, error) {
if tx == nil {
tx = database.GetDB()
}
var rows []model.InboundFallback
err := tx.Where("master_id = ?", masterId).
Order("sort_order ASC, id ASC").
Find(&rows).Error
if err != nil {
return nil, err
}
if len(rows) == 0 {
return nil, nil
}
childIds := make([]int, 0, len(rows))
for i := range rows {
childIds = append(childIds, rows[i].ChildId)
}
var children []model.Inbound
if err := tx.Where("id IN ?", childIds).Find(&children).Error; err != nil {
return nil, err
}
byId := make(map[int]*model.Inbound, len(children))
for i := range children {
byId[children[i].Id] = &children[i]
}
out := make([]map[string]any, 0, len(rows))
for _, r := range rows {
child, ok := byId[r.ChildId]
if !ok {
continue
}
listen := strings.TrimSpace(child.Listen)
if listen == "" || listen == "0.0.0.0" || listen == "::" || listen == "::0" {
listen = "127.0.0.1"
}
entry := map[string]any{
"dest": fmt.Sprintf("%s:%d", listen, child.Port),
}
if r.Name != "" {
entry["name"] = r.Name
}
if r.Alpn != "" {
entry["alpn"] = r.Alpn
}
if r.Path != "" {
entry["path"] = r.Path
}
if r.Xver > 0 {
entry["xver"] = r.Xver
}
out = append(out, entry)
}
return out, nil
}