feat(inbounds): add Port-with-Fallback inbound type

Adds a new "portfallback" protocol that emits as a VLESS-TLS inbound
under the hood but is paired with a sidecar table of child inbounds.
Panel auto-builds settings.fallbacks at Xray-config-gen time from the
sidecar — each child's listen+port becomes the fallback dest, with
SNI/ALPN/path/xver match criteria pulled from the row. No more typing
loopback ports by hand or keeping settings.fallbacks in sync.

Backend: new FallbackService (Get/SetChildren, BuildFallbacksJSON);
two new routes (GET/POST /panel/api/inbounds/:id/fallbackChildren);
xray.GetXrayConfig injects fallbacks for PortFallback inbounds; the
inbound model emits protocol="vless" so Xray accepts the config.

Frontend: PORTFALLBACK joins the protocol dropdown; selecting it
shows the standard VLESS controls plus a Fallback Children table
(inbound picker + per-row SNI/ALPN/path/xver). Children are loaded
on edit and replaced atomically on save.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-17 07:44:01 +02:00
parent 2bcf287cf1
commit 62fd9f9d82
10 changed files with 380 additions and 28 deletions
+41 -2
View File
@@ -18,8 +18,9 @@ import (
// InboundController handles HTTP requests related to Xray inbounds management.
type InboundController struct {
inboundService service.InboundService
xrayService service.XrayService
inboundService service.InboundService
xrayService service.XrayService
fallbackService service.FallbackService
}
// NewInboundController creates a new InboundController and sets up its routes.
@@ -87,6 +88,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/lastOnline", a.lastOnline)
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
g.GET("/:id/fallbackChildren", a.getFallbackChildren)
g.POST("/:id/fallbackChildren", a.setFallbackChildren)
}
type CopyInboundClientsRequest struct {
@@ -632,6 +635,42 @@ func (a *InboundController) getSubLinks(c *gin.Context) {
jsonObj(c, links, nil)
}
func (a *InboundController) getFallbackChildren(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
rows, err := a.fallbackService.GetChildren(id)
if err != nil {
jsonMsg(c, I18nWeb(c, "get"), err)
return
}
jsonObj(c, rows, nil)
}
func (a *InboundController) setFallbackChildren(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
type body struct {
Children []service.FallbackChildInput `json:"children"`
}
var b body
if err := c.ShouldBindJSON(&b); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
if err := a.fallbackService.SetChildren(id, b.Children); err != nil {
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
return
}
a.xrayService.SetToNeedRestart()
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
}
// getClientLinks returns the URL(s) for one client on one inbound — the same
// string the Copy URL button copies in the panel UI. Empty array when the
// protocol has no URL form, or when the email isn't found on the inbound.