feat: manual landing endpoints for forward nodes (item 8)
A forward node's exit target is now a choice: a managed panel server (existing behavior) or a manual landing endpoint — address + port + protocol (VLESS / SS2022 / SOCKS5) + operator-supplied credentials, including third-party nodes the panel does not run. The panel only builds the entry-side sing-box outbound and route rule; there is no exit resource, no exit job, and the node is exit-ready by definition. - model: Node.ExitMode/ExitAddress; manual exits force fixed port mode and skip the exit-server port table entirely - validation: address/port required; per-protocol credential presence; SS2022 passwords must be base64 key material of the method's key length (one PSK segment or serverKey:userKey) — the exact form sing-box accepts, rejected up front instead of failing at deploy - orchestration: resource building, deploy validation and rebuilds all branch on manualExit; switching managed->manual queues removal of the old exit resource, manual->managed mints a fresh exit resource id (deploy validation previously dereferenced the nil exit server — regression test locks the fix) - UI: exit target segmented control, link paste box that parses vless:// / ss:// / socks5:// links into the form, per-protocol credential fields (blank keeps the stored value on edit) - golden: new manualCreate/manualAssign/manualDeploy/manualJobs/ manualNodes sections — exactly one entry-side job whose outbound targets the manual address; all prior sections unchanged - the generated combined sing-box config passes the real sing-box 1.13.21 check
This commit is contained in:
@@ -59,6 +59,13 @@ func forwardNode(node *Node) bool {
|
||||
return topology == "forward"
|
||||
}
|
||||
|
||||
// manualExit reports whether a forward node's exit is an unmanaged landing
|
||||
// endpoint (round-2 item 8): no exit server, no exit resource, nothing to
|
||||
// deploy on the far side.
|
||||
func manualExit(node *Node) bool {
|
||||
return forwardNode(node) && node.ExitMode == "manual"
|
||||
}
|
||||
|
||||
func tagForNode(node *Node) string {
|
||||
var sb strings.Builder
|
||||
for _, r := range node.ID {
|
||||
@@ -263,7 +270,10 @@ func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any
|
||||
return nil, err
|
||||
}
|
||||
var exitServer *Server
|
||||
if forwardNode(node) {
|
||||
exitAddress := ""
|
||||
if manualExit(node) {
|
||||
exitAddress = node.ExitAddress
|
||||
} else if forwardNode(node) {
|
||||
exitServer = orchFindServer(d, xbDeref(node.ExitServerID))
|
||||
if exitServer == nil {
|
||||
return nil, orchNotFound("出口服务器")
|
||||
@@ -295,6 +305,7 @@ func resourceForNode(d *Data, node *Node, clients []*NodeClient) (map[string]any
|
||||
Server: server,
|
||||
BindAddress: bindAddress,
|
||||
ExitServer: exitServer,
|
||||
ExitAddress: exitAddress,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -356,7 +367,7 @@ func RebuildNode(d *Data, node *Node, includeExit bool) error {
|
||||
}
|
||||
payload := payloadWithResource(resource, "node", KeyMaterialForNode(d, node))
|
||||
QueueJob(d, node.ServerID, node.ID, "apply_resource", payload)
|
||||
if forwardNode(node) && includeExit {
|
||||
if forwardNode(node) && includeExit && !manualExit(node) {
|
||||
exitResource, err := resourceForNodeExit(d, node)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -376,7 +387,7 @@ func DeriveNodeStatus(node *Node) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if forwardNode(node) && !node.ExitReady {
|
||||
if forwardNode(node) && !node.ExitReady && !manualExit(node) {
|
||||
if node.Status != "degraded" {
|
||||
node.Status = "deploying"
|
||||
}
|
||||
@@ -631,14 +642,14 @@ func ValidateNodeDeploy(d *Data, node *Node) error {
|
||||
return orchNotFound("服务器")
|
||||
}
|
||||
var exitServer *Server
|
||||
if !direct {
|
||||
if !direct && !manualExit(node) {
|
||||
exitServer = orchFindServer(d, xbDeref(node.ExitServerID))
|
||||
if exitServer == nil {
|
||||
return orchNotFound("出口服务器")
|
||||
}
|
||||
}
|
||||
targets := []*Server{server}
|
||||
if !direct {
|
||||
if exitServer != nil {
|
||||
targets = append(targets, exitServer)
|
||||
}
|
||||
for _, target := range targets {
|
||||
@@ -691,7 +702,7 @@ func ValidateNodeDeploy(d *Data, node *Node) error {
|
||||
}
|
||||
}
|
||||
servers := []*Server{server}
|
||||
if !direct {
|
||||
if exitServer != nil {
|
||||
servers = []*Server{server, exitServer}
|
||||
}
|
||||
nowMs := time.Now().UnixMilli()
|
||||
@@ -736,7 +747,7 @@ func ValidateNodeDeploy(d *Data, node *Node) error {
|
||||
if NodePortInUse(d, node.ServerID, node.Port, node.ID) {
|
||||
return httpErr(409, "端口 %d 已被该服务器上的其他节点占用", node.Port)
|
||||
}
|
||||
if !direct && node.ExitPort != nil && NodePortInUse(d, xbDeref(node.ExitServerID), *node.ExitPort, node.ID) {
|
||||
if !direct && !manualExit(node) && node.ExitPort != nil && NodePortInUse(d, xbDeref(node.ExitServerID), *node.ExitPort, node.ID) {
|
||||
return httpErr(409, "出口端口 %d 已被该服务器上的其他节点占用", *node.ExitPort)
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user