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
+23 -1
View File
@@ -6,6 +6,7 @@ import (
"runtime"
"sync"
"github.com/mhsanaei/3x-ui/v3/database/model"
"github.com/mhsanaei/3x-ui/v3/logger"
"github.com/mhsanaei/3x-ui/v3/xray"
@@ -166,8 +167,29 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
finalClients = append(finalClients, entry)
}
if _, hadClients := settings["clients"]; hadClients || len(finalClients) > 0 {
_, hadClients := settings["clients"]
mutated := hadClients || len(finalClients) > 0
if mutated {
settings["clients"] = finalClients
}
if inbound.Protocol == model.PortFallback {
fallbacks, fbErr := s.inboundService.fallbackService.BuildFallbacksJSON(nil, inbound.Id)
if fbErr != nil {
return nil, fbErr
}
generic := make([]any, 0, len(fallbacks))
for _, f := range fallbacks {
generic = append(generic, f)
}
settings["fallbacks"] = generic
if _, ok := settings["decryption"]; !ok {
settings["decryption"] = "none"
}
mutated = true
}
if mutated {
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err