P9(3): per-inbound forwarding module (amo-style form, dynamic egress compile)
Design as decided 2026-10-21 (replaces the raw-JSON egress editor idea): the forwarding form lives on the inbound, and the entry agent's egress entries are compiled from it on every config pull — never snapshotted. - model: Inbound.Forwarding (JSON text) via AutoMigrate; panel-side metadata only, never reaches xray itself - service/forwarding.go: ForwardingSpec (mode direct/forward; landing managed|manual), save-time validation (managed target must exist and be a SOCKS-family landing — the fork's SK5 inbound is 'mixed'; no self-forward; manual endpoint/protocol sanity), live resolution (managed: concrete listen IP wins, else hosting node's address; credentials from the target's first account), and the compiler that appends one socks/http outbound (tag fwd-<inboundId>) plus a prepended inboundTag rule per forwarding inbound - buildXrayConfig agent path: compile runs after applyAgentEgress, so entries ride on the live template (or stored egress override) and template edits keep flowing; unresolvable entries skip with a warning instead of failing the whole config - UpdateInbound's field-merge now carries Forwarding (its absence was caught by the smoke: adds worked, updates silently kept the old spec) - GET /panel/api/inbounds/forwardTargets: managed-landing candidates (id/remark/tag/listen/port/node name) — credentials never exposed - frontend: new Forwarding tab in the inbound form (mode, landing type, managed picker, manual endpoint with link import incl. base64 form), adapter round-trips the spec as a JSON string like settings; en/zh strings; generated API types refreshed Acceptance: unit tests for parse/validate/resolve/compile incl. the dynamic-recompile proof (template edit + spec edit both reflected), and an end-to-end smoke (real binary, two agent nodes, all inbound writes in browser form-urlencoded format): 22/22 PASS — see P9-REPORT.md.
This commit is contained in:
@@ -67,6 +67,7 @@ func (a *InboundController) broadcastInboundsUpdate(userId int) {
|
||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/list", a.getInbounds)
|
||||
g.GET("/list/slim", a.getInboundsSlim)
|
||||
g.GET("/forwardTargets", a.getForwardTargets)
|
||||
g.GET("/options", a.getInboundOptions)
|
||||
g.GET("/allLinks", a.getAllInboundLinks)
|
||||
g.GET("/get/:id", a.getInbound)
|
||||
@@ -97,6 +98,60 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
jsonObj(c, inbounds, nil)
|
||||
}
|
||||
|
||||
// forwardTarget is one selectable managed landing for the inbound
|
||||
// forwarding form. Credentials are deliberately absent: the landing's
|
||||
// address/port/account are resolved server-side when the entry agent's
|
||||
// config is generated.
|
||||
type forwardTarget struct {
|
||||
Id int `json:"id"`
|
||||
Remark string `json:"remark"`
|
||||
Tag string `json:"tag"`
|
||||
Protocol string `json:"protocol"`
|
||||
Listen string `json:"listen"`
|
||||
Port int `json:"port"`
|
||||
Enable bool `json:"enable"`
|
||||
NodeId *int `json:"nodeId"`
|
||||
NodeName string `json:"nodeName"`
|
||||
}
|
||||
|
||||
// getForwardTargets lists the SOCKS-family (mixed) inbounds that can be
|
||||
// picked as a managed forwarding landing.
|
||||
func (a *InboundController) getForwardTargets(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbounds, err := a.inboundService.GetInbounds(user.Id)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
|
||||
return
|
||||
}
|
||||
nodeService := service.NodeService{}
|
||||
nodes, _ := nodeService.GetAll()
|
||||
nodeNames := make(map[int]string, len(nodes))
|
||||
for _, n := range nodes {
|
||||
nodeNames[n.Id] = n.Name
|
||||
}
|
||||
targets := make([]forwardTarget, 0)
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Protocol != model.Mixed && inbound.Protocol != model.Protocol("socks") {
|
||||
continue
|
||||
}
|
||||
t := forwardTarget{
|
||||
Id: inbound.Id,
|
||||
Remark: inbound.Remark,
|
||||
Tag: inbound.Tag,
|
||||
Protocol: string(inbound.Protocol),
|
||||
Listen: inbound.Listen,
|
||||
Port: inbound.Port,
|
||||
Enable: inbound.Enable,
|
||||
NodeId: inbound.NodeID,
|
||||
}
|
||||
if inbound.NodeID != nil {
|
||||
t.NodeName = nodeNames[*inbound.NodeID]
|
||||
}
|
||||
targets = append(targets, t)
|
||||
}
|
||||
jsonObj(c, targets, nil)
|
||||
}
|
||||
|
||||
// getInboundsSlim is the list-page variant that strips full client
|
||||
// payloads from settings.clients[]. Detail-view flows still use /get/:id.
|
||||
func (a *InboundController) getInboundsSlim(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user