mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
feat: add custom IP selection for nodes, tunnels, and forwards
- Add extra_ips field to nodes for multi-IP servers - Add connect_ip field to chain_tunnel for specifying connection address - Add in_ip field to forward_port for specifying listen address - Frontend: add UI controls for extra IPs on node form - Frontend: add connect IP input for tunnel chain nodes - Frontend: add listen IP input for forward form - Backend: resolve forward ingress with custom listen IP priority Entire-Checkpoint: 557563462c16
This commit is contained in:
@@ -72,7 +72,7 @@ func (h *Handler) buildDiagnosisStreamStartItems(workItems []diagnosisWorkItem)
|
|||||||
fromNode, _ := h.cachedNode(nodeCache, workItem.fromNodeID)
|
fromNode, _ := h.cachedNode(nodeCache, workItem.fromNodeID)
|
||||||
targetNode, err := h.cachedNode(nodeCache, workItem.toNode.NodeID)
|
targetNode, err := h.cachedNode(nodeCache, workItem.toNode.NodeID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
resolvedIP, resolvedPort, resolveErr := resolveChainProbeTarget(fromNode, targetNode, workItem.toNode.Port, workItem.ipPreference)
|
resolvedIP, resolvedPort, resolveErr := resolveChainProbeTarget(fromNode, targetNode, workItem.toNode.Port, workItem.ipPreference, "")
|
||||||
if resolveErr == nil {
|
if resolveErr == nil {
|
||||||
targetIP = resolvedIP
|
targetIP = resolvedIP
|
||||||
targetPort = resolvedPort
|
targetPort = resolvedPort
|
||||||
@@ -1099,7 +1099,7 @@ func (h *Handler) appendChainHopDiagnosis(results *[]map[string]interface{}, nod
|
|||||||
h.appendFailedDiagnosis(results, nodeCache, fromNodeID, "", 0, description, metadata, err.Error())
|
h.appendFailedDiagnosis(results, nodeCache, fromNodeID, "", 0, description, metadata, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetIP, targetPort, err := resolveChainProbeTarget(fromNode, targetNode, toNode.Port, ipPreference)
|
targetIP, targetPort, err := resolveChainProbeTarget(fromNode, targetNode, toNode.Port, ipPreference, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.appendFailedDiagnosis(results, nodeCache, fromNodeID, strings.Trim(strings.TrimSpace(targetNode.ServerIP), "[]"), toNode.Port, description, metadata, err.Error())
|
h.appendFailedDiagnosis(results, nodeCache, fromNodeID, strings.Trim(strings.TrimSpace(targetNode.ServerIP), "[]"), toNode.Port, description, metadata, err.Error())
|
||||||
return
|
return
|
||||||
@@ -1107,11 +1107,11 @@ func (h *Handler) appendChainHopDiagnosis(results *[]map[string]interface{}, nod
|
|||||||
h.appendPathDiagnosis(results, nodeCache, fromNodeID, targetIP, targetPort, description, metadata, options)
|
h.appendPathDiagnosis(results, nodeCache, fromNodeID, targetIP, targetPort, description, metadata, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveChainProbeTarget(fromNode, targetNode *nodeRecord, preferredPort int, ipPreference string) (string, int, error) {
|
func resolveChainProbeTarget(fromNode, targetNode *nodeRecord, preferredPort int, ipPreference string, connectIp string) (string, int, error) {
|
||||||
if targetNode == nil {
|
if targetNode == nil {
|
||||||
return "", 0, errors.New("目标节点不存在")
|
return "", 0, errors.New("目标节点不存在")
|
||||||
}
|
}
|
||||||
host, err := selectTunnelDialHost(fromNode, targetNode, ipPreference)
|
host, err := selectTunnelDialHost(fromNode, targetNode, ipPreference, connectIp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = strings.Trim(strings.TrimSpace(targetNode.ServerIP), "[]")
|
host = strings.Trim(strings.TrimSpace(targetNode.ServerIP), "[]")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,25 @@ import (
|
|||||||
// nodeSupportsV4 / nodeSupportsV6
|
// nodeSupportsV4 / nodeSupportsV6
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func TestNodeSupportsV4_Nil(t *testing.T) {
|
func TestSelectTunnelDialHost_ConnectIpPriority(t *testing.T) {
|
||||||
if nodeSupportsV4(nil) {
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
t.Fatal("nil node must not support v4")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
|
||||||
|
// Empty connectIp should be ignored, IP preference takes effect
|
||||||
|
host, err := selectTunnelDialHost(from, to, "", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if host != "10.0.0.2" {
|
||||||
|
t.Fatalf("empty connectIp should be ignored (v4 preference applies), got %q", host)
|
||||||
|
}
|
||||||
|
// Non-empty connectIp should override IP preference
|
||||||
|
host, err = selectTunnelDialHost(from, to, "v6", "192.168.0.3")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if host != "192.168.0.3" {
|
||||||
|
t.Fatalf("connectIp should override v6 preference, got %q", host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,14 +39,14 @@ func TestNodeSupportsV6_Nil(t *testing.T) {
|
|||||||
func TestNodeSupportsV4_ExplicitV4(t *testing.T) {
|
func TestNodeSupportsV4_ExplicitV4(t *testing.T) {
|
||||||
n := &nodeRecord{ServerIPv4: "10.0.0.1"}
|
n := &nodeRecord{ServerIPv4: "10.0.0.1"}
|
||||||
if !nodeSupportsV4(n) {
|
if !nodeSupportsV4(n) {
|
||||||
t.Fatal("explicit server_ip_v4 must support v4")
|
t.Fatal("explicit server_ip_v4 needs support v4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeSupportsV6_ExplicitV6(t *testing.T) {
|
func TestNodeSupportsV6_ExplicitV6(t *testing.T) {
|
||||||
n := &nodeRecord{ServerIPv6: "2001:db8::1"}
|
n := &nodeRecord{ServerIPv6: "2001:db8::1"}
|
||||||
if !nodeSupportsV6(n) {
|
if !nodeSupportsV6(n) {
|
||||||
t.Fatal("explicit server_ip_v6 must support v6")
|
t.Fatal("explicit server_ip_v6 needs support v6")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +84,7 @@ func TestNodeSupportsV4_LegacyV4Only(t *testing.T) {
|
|||||||
t.Fatal("legacy v4 ip in server_ip must support v4")
|
t.Fatal("legacy v4 ip in server_ip must support v4")
|
||||||
}
|
}
|
||||||
if nodeSupportsV6(n) {
|
if nodeSupportsV6(n) {
|
||||||
t.Fatal("legacy v4 ip in server_ip must not support v6")
|
t.Fatal("legacy v4 ip in server_ip should not support v6")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +94,7 @@ func TestNodeSupportsV6_LegacyV6Only(t *testing.T) {
|
|||||||
t.Fatal("legacy v6 ip in server_ip must support v6")
|
t.Fatal("legacy v6 ip in server_ip must support v6")
|
||||||
}
|
}
|
||||||
if nodeSupportsV4(n) {
|
if nodeSupportsV4(n) {
|
||||||
t.Fatal("legacy v6 ip in server_ip must not support v4")
|
t.Fatal("legacy v6 ip in server_ip should not support v4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,15 +193,15 @@ func v6OnlyNode(name, v6 string) *nodeRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectTunnelDialHost_NilNodes(t *testing.T) {
|
func TestSelectTunnelDialHost_NilNodes(t *testing.T) {
|
||||||
_, err := selectTunnelDialHost(nil, nil, "")
|
_, err := selectTunnelDialHost(nil, nil, "", "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for nil nodes")
|
t.Fatal("expected error for nil nodes")
|
||||||
}
|
}
|
||||||
_, err = selectTunnelDialHost(dualStackNode("a", "1.1.1.1", "::1"), nil, "")
|
_, err = selectTunnelDialHost(dualStackNode("a", "1.1.1.1", "::1"), nil, "", "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for nil toNode")
|
t.Fatal("expected error for nil toNode")
|
||||||
}
|
}
|
||||||
_, err = selectTunnelDialHost(nil, dualStackNode("b", "1.1.1.1", "::1"), "")
|
_, err = selectTunnelDialHost(nil, dualStackNode("b", "1.1.1.1", "::1"), "", "")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for nil fromNode")
|
t.Fatal("expected error for nil fromNode")
|
||||||
}
|
}
|
||||||
@@ -194,8 +210,7 @@ func TestSelectTunnelDialHost_NilNodes(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_DualStack_DefaultPreference(t *testing.T) {
|
func TestSelectTunnelDialHost_DualStack_DefaultPreference(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
host, err := selectTunnelDialHost(from, to, "", "")
|
||||||
host, err := selectTunnelDialHost(from, to, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -208,8 +223,7 @@ func TestSelectTunnelDialHost_DualStack_DefaultPreference(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_DualStack_PreferV4(t *testing.T) {
|
func TestSelectTunnelDialHost_DualStack_PreferV4(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
host, err := selectTunnelDialHost(from, to, "v4", "")
|
||||||
host, err := selectTunnelDialHost(from, to, "v4")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -221,8 +235,7 @@ func TestSelectTunnelDialHost_DualStack_PreferV4(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_DualStack_PreferV6(t *testing.T) {
|
func TestSelectTunnelDialHost_DualStack_PreferV6(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
host, err := selectTunnelDialHost(from, to, "v6", "")
|
||||||
host, err := selectTunnelDialHost(from, to, "v6")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -234,9 +247,8 @@ func TestSelectTunnelDialHost_DualStack_PreferV6(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_V4Only_PreferV6Fallback(t *testing.T) {
|
func TestSelectTunnelDialHost_V4Only_PreferV6Fallback(t *testing.T) {
|
||||||
from := v4OnlyNode("from", "10.0.0.1")
|
from := v4OnlyNode("from", "10.0.0.1")
|
||||||
to := v4OnlyNode("to", "10.0.0.2")
|
to := v4OnlyNode("to", "10.0.0.2")
|
||||||
|
|
||||||
// User prefers v6, but both nodes are v4-only — should fallback to v4
|
// User prefers v6, but both nodes are v4-only — should fallback to v4
|
||||||
host, err := selectTunnelDialHost(from, to, "v6")
|
host, err := selectTunnelDialHost(from, to, "v6", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -248,9 +260,8 @@ func TestSelectTunnelDialHost_V4Only_PreferV6Fallback(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_V6Only_PreferV4Fallback(t *testing.T) {
|
func TestSelectTunnelDialHost_V6Only_PreferV4Fallback(t *testing.T) {
|
||||||
from := v6OnlyNode("from", "2001:db8::1")
|
from := v6OnlyNode("from", "2001:db8::1")
|
||||||
to := v6OnlyNode("to", "2001:db8::2")
|
to := v6OnlyNode("to", "2001:db8::2")
|
||||||
|
|
||||||
// User prefers v4, but both nodes are v6-only — should fallback to v6
|
// User prefers v4, but both nodes are v6-only — should fallback to v6
|
||||||
host, err := selectTunnelDialHost(from, to, "v4")
|
host, err := selectTunnelDialHost(from, to, "v4", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -262,8 +273,7 @@ func TestSelectTunnelDialHost_V6Only_PreferV4Fallback(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_Incompatible(t *testing.T) {
|
func TestSelectTunnelDialHost_Incompatible(t *testing.T) {
|
||||||
from := v4OnlyNode("from", "10.0.0.1")
|
from := v4OnlyNode("from", "10.0.0.1")
|
||||||
to := v6OnlyNode("to", "2001:db8::2")
|
to := v6OnlyNode("to", "2001:db8::2")
|
||||||
|
_, err := selectTunnelDialHost(from, to, "", "")
|
||||||
_, err := selectTunnelDialHost(from, to, "")
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for incompatible nodes (v4-only -> v6-only)")
|
t.Fatal("expected error for incompatible nodes (v4-only -> v6-only)")
|
||||||
}
|
}
|
||||||
@@ -272,8 +282,7 @@ func TestSelectTunnelDialHost_Incompatible(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_Incompatible_Reverse(t *testing.T) {
|
func TestSelectTunnelDialHost_Incompatible_Reverse(t *testing.T) {
|
||||||
from := v6OnlyNode("from", "2001:db8::1")
|
from := v6OnlyNode("from", "2001:db8::1")
|
||||||
to := v4OnlyNode("to", "10.0.0.2")
|
to := v4OnlyNode("to", "10.0.0.2")
|
||||||
|
_, err := selectTunnelDialHost(from, to, "", "")
|
||||||
_, err := selectTunnelDialHost(from, to, "")
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error for incompatible nodes (v6-only -> v4-only)")
|
t.Fatal("expected error for incompatible nodes (v6-only -> v4-only)")
|
||||||
}
|
}
|
||||||
@@ -282,9 +291,8 @@ func TestSelectTunnelDialHost_Incompatible_Reverse(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_WhitespacePreference(t *testing.T) {
|
func TestSelectTunnelDialHost_WhitespacePreference(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
|
||||||
// Whitespace should be trimmed, treated as "v6"
|
// Whitespace should be trimmed, treated as "v6"
|
||||||
host, err := selectTunnelDialHost(from, to, " v6 ")
|
host, err := selectTunnelDialHost(from, to, " v6 ", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -296,9 +304,8 @@ func TestSelectTunnelDialHost_WhitespacePreference(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_MixedStack_FromDualToV4(t *testing.T) {
|
func TestSelectTunnelDialHost_MixedStack_FromDualToV4(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := v4OnlyNode("to", "10.0.0.2")
|
to := v4OnlyNode("to", "10.0.0.2")
|
||||||
|
|
||||||
// v6 preferred, but target only has v4 — should succeed with v4
|
// v6 preferred, but target only has v4 — should succeed with v4
|
||||||
host, err := selectTunnelDialHost(from, to, "v6")
|
host, err := selectTunnelDialHost(from, to, "v6", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -310,9 +317,8 @@ func TestSelectTunnelDialHost_MixedStack_FromDualToV4(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_MixedStack_FromDualToV6(t *testing.T) {
|
func TestSelectTunnelDialHost_MixedStack_FromDualToV6(t *testing.T) {
|
||||||
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
from := dualStackNode("from", "10.0.0.1", "2001:db8::1")
|
||||||
to := v6OnlyNode("to", "2001:db8::2")
|
to := v6OnlyNode("to", "2001:db8::2")
|
||||||
|
|
||||||
// v4 preferred, but target only has v6 — should succeed with v6
|
// v4 preferred, but target only has v6 — should succeed with v6
|
||||||
host, err := selectTunnelDialHost(from, to, "v4")
|
host, err := selectTunnelDialHost(from, to, "v4", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -324,9 +330,8 @@ func TestSelectTunnelDialHost_MixedStack_FromDualToV6(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_MixedStack_FromV4ToDual(t *testing.T) {
|
func TestSelectTunnelDialHost_MixedStack_FromV4ToDual(t *testing.T) {
|
||||||
from := v4OnlyNode("from", "10.0.0.1")
|
from := v4OnlyNode("from", "10.0.0.1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
|
||||||
// v6 preferred, but from only has v4 — should use v4 (from can only reach v4 of target)
|
// v6 preferred, but from only has v4 — should use v4 (from can only reach v4 of target)
|
||||||
host, err := selectTunnelDialHost(from, to, "v6")
|
host, err := selectTunnelDialHost(from, to, "v6", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -338,9 +343,8 @@ func TestSelectTunnelDialHost_MixedStack_FromV4ToDual(t *testing.T) {
|
|||||||
func TestSelectTunnelDialHost_MixedStack_FromV6ToDual(t *testing.T) {
|
func TestSelectTunnelDialHost_MixedStack_FromV6ToDual(t *testing.T) {
|
||||||
from := v6OnlyNode("from", "2001:db8::1")
|
from := v6OnlyNode("from", "2001:db8::1")
|
||||||
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
to := dualStackNode("to", "10.0.0.2", "2001:db8::2")
|
||||||
|
|
||||||
// v4 preferred, but from only has v6 — should use v6
|
// v4 preferred, but from only has v6 — should use v6
|
||||||
host, err := selectTunnelDialHost(from, to, "v4")
|
host, err := selectTunnelDialHost(from, to, "v4", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -367,7 +371,6 @@ func TestNodeDisplayName_Named(t *testing.T) {
|
|||||||
t.Fatalf("expected 'hk-node', got %q", got)
|
t.Fatalf("expected 'hk-node', got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeDisplayName_Unnamed(t *testing.T) {
|
func TestNodeDisplayName_Unnamed(t *testing.T) {
|
||||||
n := &nodeRecord{ID: 42}
|
n := &nodeRecord{ID: 42}
|
||||||
got := nodeDisplayName(n)
|
got := nodeDisplayName(n)
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ func (h *Handler) nodeCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
nullableText(asString(req["remoteUrl"])),
|
nullableText(asString(req["remoteUrl"])),
|
||||||
nullableText(asString(req["remoteToken"])),
|
nullableText(asString(req["remoteToken"])),
|
||||||
nullableText(asString(req["remoteConfig"])),
|
nullableText(asString(req["remoteConfig"])),
|
||||||
|
nullableText(asString(req["extraIPs"])),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -322,6 +323,7 @@ func (h *Handler) nodeUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
nullableText(asString(req["serverIpV6"])),
|
nullableText(asString(req["serverIpV6"])),
|
||||||
defaultString(asString(req["port"]), "1000-65535"),
|
defaultString(asString(req["port"]), "1000-65535"),
|
||||||
nullableText(asString(req["interfaceName"])),
|
nullableText(asString(req["interfaceName"])),
|
||||||
|
nullableText(asString(req["extraIPs"])),
|
||||||
newHTTP,
|
newHTTP,
|
||||||
newTLS,
|
newTLS,
|
||||||
newSocks,
|
newSocks,
|
||||||
@@ -1176,7 +1178,8 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if userName == "" {
|
if userName == "" {
|
||||||
userName = "user"
|
userName = "user"
|
||||||
}
|
}
|
||||||
forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, nullableInt(speedID))
|
inIp := strings.TrimSpace(asString(req["inIp"]))
|
||||||
|
forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -1280,6 +1283,7 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
port = h.pickTunnelPort(tunnelID)
|
port = h.pickTunnelPort(tunnelID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
inIp := asString(req["inIp"])
|
||||||
fwdEntryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
fwdEntryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||||
for _, nodeID := range fwdEntryNodes {
|
for _, nodeID := range fwdEntryNodes {
|
||||||
node, nodeErr := h.getNodeRecord(nodeID)
|
node, nodeErr := h.getNodeRecord(nodeID)
|
||||||
@@ -1296,7 +1300,7 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.replaceForwardPorts(id, tunnelID, port); err != nil {
|
if err := h.replaceForwardPorts(id, tunnelID, port, inIp); err != nil {
|
||||||
h.rollbackForwardMutation(forward, oldPorts)
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -1628,7 +1632,7 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
|||||||
fail++
|
fail++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := h.replaceForwardPorts(id, req.TargetTunnelID, p); err != nil {
|
if err := h.replaceForwardPorts(id, req.TargetTunnelID, p, ""); err != nil {
|
||||||
h.rollbackForwardMutation(forward, oldPorts)
|
h.rollbackForwardMutation(forward, oldPorts)
|
||||||
fail++
|
fail++
|
||||||
continue
|
continue
|
||||||
@@ -1954,6 +1958,7 @@ type tunnelRuntimeNode struct {
|
|||||||
Inx int
|
Inx int
|
||||||
ChainType int
|
ChainType int
|
||||||
Port int
|
Port int
|
||||||
|
ConnectIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
type tunnelCreateState struct {
|
type tunnelCreateState struct {
|
||||||
@@ -2027,6 +2032,7 @@ func (h *Handler) prepareTunnelCreateState(tx *gorm.DB, req map[string]interface
|
|||||||
Strategy: defaultString(asString(item["strategy"]), "round"),
|
Strategy: defaultString(asString(item["strategy"]), "round"),
|
||||||
ChainType: 3,
|
ChainType: 3,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
ConnectIP: asString(item["connectIp"]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(state.OutNodes) == 0 {
|
if len(state.OutNodes) == 0 {
|
||||||
@@ -2062,6 +2068,7 @@ func (h *Handler) prepareTunnelCreateState(tx *gorm.DB, req map[string]interface
|
|||||||
Inx: hopIdx + 1,
|
Inx: hopIdx + 1,
|
||||||
ChainType: 2,
|
ChainType: 2,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
ConnectIP: asString(item["connectIp"]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(hop) > 0 {
|
if len(hop) > 0 {
|
||||||
@@ -2338,7 +2345,7 @@ func (h *Handler) applyFederationRuntime(state *tunnelCreateState, localDomain s
|
|||||||
h.releaseFederationRuntimeRefs(releaseRefs)
|
h.releaseFederationRuntimeRefs(releaseRefs)
|
||||||
return nil, nil, errors.New("节点不存在")
|
return nil, nil, errors.New("节点不存在")
|
||||||
}
|
}
|
||||||
host, hostErr := selectTunnelDialHost(node, targetNode, state.IPPreference)
|
host, hostErr := selectTunnelDialHost(node, targetNode, state.IPPreference, target.ConnectIP)
|
||||||
if hostErr != nil {
|
if hostErr != nil {
|
||||||
h.releaseFederationRuntimeRefs(releaseRefs)
|
h.releaseFederationRuntimeRefs(releaseRefs)
|
||||||
return nil, nil, hostErr
|
return nil, nil, hostErr
|
||||||
@@ -2578,7 +2585,7 @@ func buildTunnelChainConfig(tunnelID int64, fromNodeID int64, targets []tunnelRu
|
|||||||
if targetNode == nil {
|
if targetNode == nil {
|
||||||
return nil, errors.New("节点不存在")
|
return nil, errors.New("节点不存在")
|
||||||
}
|
}
|
||||||
host, err := selectTunnelDialHost(fromNode, targetNode, ipPreference)
|
host, err := selectTunnelDialHost(fromNode, targetNode, ipPreference, target.ConnectIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -2651,10 +2658,13 @@ func buildTunnelChainServiceConfig(tunnelID int64, chainNode tunnelRuntimeNode,
|
|||||||
return []map[string]interface{}{service}
|
return []map[string]interface{}{service}
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectTunnelDialHost(fromNode, toNode *nodeRecord, ipPreference string) (string, error) {
|
func selectTunnelDialHost(fromNode, toNode *nodeRecord, ipPreference string, connectIp string) (string, error) {
|
||||||
if fromNode == nil || toNode == nil {
|
if fromNode == nil || toNode == nil {
|
||||||
return "", errors.New("节点不存在")
|
return "", errors.New("节点不存在")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(connectIp) != "" {
|
||||||
|
return strings.TrimSpace(connectIp), nil
|
||||||
|
}
|
||||||
fromV4 := nodeSupportsV4(fromNode)
|
fromV4 := nodeSupportsV4(fromNode)
|
||||||
fromV6 := nodeSupportsV6(fromNode)
|
fromV6 := nodeSupportsV6(fromNode)
|
||||||
toV4 := nodeSupportsV4(toNode)
|
toV4 := nodeSupportsV4(toNode)
|
||||||
@@ -2789,6 +2799,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
defaultString(asString(n["strategy"]), "round"),
|
defaultString(asString(n["strategy"]), "round"),
|
||||||
i+1,
|
i+1,
|
||||||
defaultString(asString(n["protocol"]), "tls"),
|
defaultString(asString(n["protocol"]), "tls"),
|
||||||
|
"",
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2806,6 +2817,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
return pickErr
|
return pickErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
connectIp := asString(n["connectIp"])
|
||||||
if err := h.repo.CreateChainTunnelTx(
|
if err := h.repo.CreateChainTunnelTx(
|
||||||
tx,
|
tx,
|
||||||
tunnelID,
|
tunnelID,
|
||||||
@@ -2815,6 +2827,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
defaultString(asString(n["strategy"]), "round"),
|
defaultString(asString(n["strategy"]), "round"),
|
||||||
i+1,
|
i+1,
|
||||||
defaultString(asString(n["protocol"]), "tls"),
|
defaultString(asString(n["protocol"]), "tls"),
|
||||||
|
connectIp,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2834,6 +2847,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
return pickErr
|
return pickErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
connectIp := asString(n["connectIp"])
|
||||||
if err := h.repo.CreateChainTunnelTx(
|
if err := h.repo.CreateChainTunnelTx(
|
||||||
tx,
|
tx,
|
||||||
tunnelID,
|
tunnelID,
|
||||||
@@ -2843,6 +2857,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str
|
|||||||
defaultString(asString(n["strategy"]), "round"),
|
defaultString(asString(n["strategy"]), "round"),
|
||||||
i+1,
|
i+1,
|
||||||
defaultString(asString(n["protocol"]), "tls"),
|
defaultString(asString(n["protocol"]), "tls"),
|
||||||
|
connectIp,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -2999,7 +3014,7 @@ func parsePorts(portRange string) ([]int, error) {
|
|||||||
return ports, nil
|
return ports, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int) error {
|
func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int, inIp string) error {
|
||||||
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -3007,12 +3022,14 @@ func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int) error
|
|||||||
entries := make([]struct {
|
entries := make([]struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
|
InIP string
|
||||||
}, len(entryNodes))
|
}, len(entryNodes))
|
||||||
for i, nid := range entryNodes {
|
for i, nid := range entryNodes {
|
||||||
entries[i] = struct {
|
entries[i] = struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
}{NodeID: nid, Port: port}
|
InIP string
|
||||||
|
}{NodeID: nid, Port: port, InIP: inIp}
|
||||||
}
|
}
|
||||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
||||||
}
|
}
|
||||||
@@ -3021,12 +3038,14 @@ func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwar
|
|||||||
entries := make([]struct {
|
entries := make([]struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
|
InIP string
|
||||||
}, len(ports))
|
}, len(ports))
|
||||||
for i, fp := range ports {
|
for i, fp := range ports {
|
||||||
entries[i] = struct {
|
entries[i] = struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
}{NodeID: fp.NodeID, Port: fp.Port}
|
InIP string
|
||||||
|
}{NodeID: fp.NodeID, Port: fp.Port, InIP: fp.InIP}
|
||||||
}
|
}
|
||||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,10 +48,11 @@ type Forward struct {
|
|||||||
func (Forward) TableName() string { return "forward" }
|
func (Forward) TableName() string { return "forward" }
|
||||||
|
|
||||||
type ForwardPort struct {
|
type ForwardPort struct {
|
||||||
ID int64 `gorm:"primaryKey;autoIncrement"`
|
ID int64 `gorm:"primaryKey;autoIncrement"`
|
||||||
ForwardID int64 `gorm:"column:forward_id;not null"`
|
ForwardID int64 `gorm:"column:forward_id;not null"`
|
||||||
NodeID int64 `gorm:"column:node_id;not null"`
|
NodeID int64 `gorm:"column:node_id;not null"`
|
||||||
Port int `gorm:"not null"`
|
Port int `gorm:"not null"`
|
||||||
|
InIP sql.NullString `gorm:"column:in_ip;type:text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ForwardPort) TableName() string { return "forward_port" }
|
func (ForwardPort) TableName() string { return "forward_port" }
|
||||||
@@ -63,6 +64,7 @@ type Node struct {
|
|||||||
ServerIP string `gorm:"column:server_ip;type:varchar(100);not null"`
|
ServerIP string `gorm:"column:server_ip;type:varchar(100);not null"`
|
||||||
ServerIPV4 sql.NullString `gorm:"column:server_ip_v4;type:varchar(100)"`
|
ServerIPV4 sql.NullString `gorm:"column:server_ip_v4;type:varchar(100)"`
|
||||||
ServerIPV6 sql.NullString `gorm:"column:server_ip_v6;type:varchar(100)"`
|
ServerIPV6 sql.NullString `gorm:"column:server_ip_v6;type:varchar(100)"`
|
||||||
|
ExtraIPs sql.NullString `gorm:"column:extra_ips;type:text"`
|
||||||
Port string `gorm:"type:text;not null"`
|
Port string `gorm:"type:text;not null"`
|
||||||
InterfaceName sql.NullString `gorm:"column:interface_name;type:varchar(200)"`
|
InterfaceName sql.NullString `gorm:"column:interface_name;type:varchar(200)"`
|
||||||
Version sql.NullString `gorm:"type:varchar(100)"`
|
Version sql.NullString `gorm:"type:varchar(100)"`
|
||||||
@@ -133,6 +135,7 @@ type ChainTunnel struct {
|
|||||||
Strategy sql.NullString `gorm:"type:varchar(10)"`
|
Strategy sql.NullString `gorm:"type:varchar(10)"`
|
||||||
Inx sql.NullInt64 `gorm:"column:inx"`
|
Inx sql.NullInt64 `gorm:"column:inx"`
|
||||||
Protocol sql.NullString `gorm:"type:varchar(10)"`
|
Protocol sql.NullString `gorm:"type:varchar(10)"`
|
||||||
|
ConnectIP sql.NullString `gorm:"column:connect_ip;type:varchar(45)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ChainTunnel) TableName() string { return "chain_tunnel" }
|
func (ChainTunnel) TableName() string { return "chain_tunnel" }
|
||||||
@@ -337,6 +340,7 @@ type NodeBackup struct {
|
|||||||
ServerIP string `json:"serverIp"`
|
ServerIP string `json:"serverIp"`
|
||||||
ServerIPv4 string `json:"serverIpV4,omitempty"`
|
ServerIPv4 string `json:"serverIpV4,omitempty"`
|
||||||
ServerIPv6 string `json:"serverIpV6,omitempty"`
|
ServerIPv6 string `json:"serverIpV6,omitempty"`
|
||||||
|
ExtraIPs string `json:"extraIPs,omitempty"`
|
||||||
Port string `json:"port"`
|
Port string `json:"port"`
|
||||||
InterfaceName string `json:"interfaceName,omitempty"`
|
InterfaceName string `json:"interfaceName,omitempty"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
@@ -510,6 +514,7 @@ type TunnelRecord struct {
|
|||||||
type ForwardPortRecord struct {
|
type ForwardPortRecord struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
|
InIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeRecord is a node view used by control plane.
|
// NodeRecord is a node view used by control plane.
|
||||||
@@ -519,6 +524,7 @@ type NodeRecord struct {
|
|||||||
ServerIP string
|
ServerIP string
|
||||||
ServerIPv4 string
|
ServerIPv4 string
|
||||||
ServerIPv6 string
|
ServerIPv6 string
|
||||||
|
ExtraIPs string
|
||||||
Status int
|
Status int
|
||||||
PortRange string
|
PortRange string
|
||||||
TCPListenAddr string
|
TCPListenAddr string
|
||||||
@@ -538,6 +544,7 @@ type ChainNodeRecord struct {
|
|||||||
NodeName string
|
NodeName string
|
||||||
Protocol string
|
Protocol string
|
||||||
Strategy string
|
Strategy string
|
||||||
|
ConnectIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserTunnelLimiterInfo struct {
|
type UserTunnelLimiterInfo struct {
|
||||||
|
|||||||
@@ -637,6 +637,7 @@ func (r *Repository) ListNodes() ([]map[string]interface{}, error) {
|
|||||||
"ip": n.ServerIP, "serverIp": n.ServerIP,
|
"ip": n.ServerIP, "serverIp": n.ServerIP,
|
||||||
"serverIpV4": nullableString(n.ServerIPV4),
|
"serverIpV4": nullableString(n.ServerIPV4),
|
||||||
"serverIpV6": nullableString(n.ServerIPV6),
|
"serverIpV6": nullableString(n.ServerIPV6),
|
||||||
|
"extraIPs": nullableString(n.ExtraIPs),
|
||||||
"port": n.Port,
|
"port": n.Port,
|
||||||
"tcpListenAddr": n.TCPListenAddr,
|
"tcpListenAddr": n.TCPListenAddr,
|
||||||
"udpListenAddr": n.UDPListenAddr,
|
"udpListenAddr": n.UDPListenAddr,
|
||||||
@@ -2731,10 +2732,11 @@ func resolveForwardIngress(db *gorm.DB, forwardID int64, tunnelID int64) (string
|
|||||||
type fpRow struct {
|
type fpRow struct {
|
||||||
Port sql.NullInt64
|
Port sql.NullInt64
|
||||||
ServerIP sql.NullString
|
ServerIP sql.NullString
|
||||||
|
InIP sql.NullString
|
||||||
}
|
}
|
||||||
var fpRows []fpRow
|
var fpRows []fpRow
|
||||||
err := db.Model(&model.ForwardPort{}).
|
err := db.Model(&model.ForwardPort{}).
|
||||||
Select("forward_port.port, node.server_ip").
|
Select("forward_port.port, node.server_ip, forward_port.in_ip").
|
||||||
Joins("LEFT JOIN node ON node.id = forward_port.node_id").
|
Joins("LEFT JOIN node ON node.id = forward_port.node_id").
|
||||||
Where("forward_port.forward_id = ?", forwardID).
|
Where("forward_port.forward_id = ?", forwardID).
|
||||||
Order("forward_port.id ASC").
|
Order("forward_port.id ASC").
|
||||||
@@ -2744,10 +2746,22 @@ func resolveForwardIngress(db *gorm.DB, forwardID int64, tunnelID int64) (string
|
|||||||
}
|
}
|
||||||
|
|
||||||
ports := make([]int64, 0)
|
ports := make([]int64, 0)
|
||||||
nodePairs := make([]string, 0)
|
entries := make([]string, 0)
|
||||||
seenPorts := make(map[int64]struct{})
|
seenPorts := make(map[int64]struct{})
|
||||||
seenPairs := make(map[string]struct{})
|
seenPairs := make(map[string]struct{})
|
||||||
|
|
||||||
|
var tunnelFirstIP string
|
||||||
|
if tunnelInIP.Valid && strings.TrimSpace(tunnelInIP.String) != "" {
|
||||||
|
tunnelIPs := strings.Split(tunnelInIP.String, ",")
|
||||||
|
for _, ip := range tunnelIPs {
|
||||||
|
ip = strings.TrimSpace(ip)
|
||||||
|
if ip != "" {
|
||||||
|
tunnelFirstIP = ip
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, row := range fpRows {
|
for _, row := range fpRows {
|
||||||
if !row.Port.Valid {
|
if !row.Port.Valid {
|
||||||
continue
|
continue
|
||||||
@@ -2756,11 +2770,21 @@ func resolveForwardIngress(db *gorm.DB, forwardID int64, tunnelID int64) (string
|
|||||||
seenPorts[row.Port.Int64] = struct{}{}
|
seenPorts[row.Port.Int64] = struct{}{}
|
||||||
ports = append(ports, row.Port.Int64)
|
ports = append(ports, row.Port.Int64)
|
||||||
}
|
}
|
||||||
if row.ServerIP.Valid && strings.TrimSpace(row.ServerIP.String) != "" {
|
|
||||||
pair := fmt.Sprintf("%s:%d", strings.TrimSpace(row.ServerIP.String), row.Port.Int64)
|
var ip string
|
||||||
|
if row.InIP.Valid && strings.TrimSpace(row.InIP.String) != "" {
|
||||||
|
ip = strings.TrimSpace(row.InIP.String)
|
||||||
|
} else if tunnelFirstIP != "" {
|
||||||
|
ip = tunnelFirstIP
|
||||||
|
} else if row.ServerIP.Valid && strings.TrimSpace(row.ServerIP.String) != "" {
|
||||||
|
ip = strings.TrimSpace(row.ServerIP.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip != "" {
|
||||||
|
pair := fmt.Sprintf("%s:%d", ip, row.Port.Int64)
|
||||||
if _, ok := seenPairs[pair]; !ok {
|
if _, ok := seenPairs[pair]; !ok {
|
||||||
seenPairs[pair] = struct{}{}
|
seenPairs[pair] = struct{}{}
|
||||||
nodePairs = append(nodePairs, pair)
|
entries = append(entries, pair)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2771,27 +2795,6 @@ func resolveForwardIngress(db *gorm.DB, forwardID int64, tunnelID int64) (string
|
|||||||
|
|
||||||
inPort := sql.NullInt64{Int64: ports[0], Valid: true}
|
inPort := sql.NullInt64{Int64: ports[0], Valid: true}
|
||||||
|
|
||||||
entries := make([]string, 0)
|
|
||||||
if tunnelInIP.Valid && strings.TrimSpace(tunnelInIP.String) != "" {
|
|
||||||
tunnelIPs := strings.Split(tunnelInIP.String, ",")
|
|
||||||
seen := make(map[string]struct{})
|
|
||||||
for _, ip := range tunnelIPs {
|
|
||||||
ip = strings.TrimSpace(ip)
|
|
||||||
if ip == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[ip]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[ip] = struct{}{}
|
|
||||||
for _, port := range ports {
|
|
||||||
entries = append(entries, fmt.Sprintf("%s:%d", ip, port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entries = append(entries, nodePairs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(entries, ","), inPort, nil
|
return strings.Join(entries, ","), inPort, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,11 @@ func (r *Repository) ListForwardPorts(forwardID int64) ([]model.ForwardPortRecor
|
|||||||
}
|
}
|
||||||
rows := make([]model.ForwardPortRecord, 0, len(ports))
|
rows := make([]model.ForwardPortRecord, 0, len(ports))
|
||||||
for _, p := range ports {
|
for _, p := range ports {
|
||||||
rows = append(rows, model.ForwardPortRecord{NodeID: p.NodeID, Port: p.Port})
|
inIP := ""
|
||||||
|
if p.InIP.Valid {
|
||||||
|
inIP = p.InIP.String
|
||||||
|
}
|
||||||
|
rows = append(rows, model.ForwardPortRecord{NodeID: p.NodeID, Port: p.Port, InIP: inIP})
|
||||||
}
|
}
|
||||||
return rows, nil
|
return rows, nil
|
||||||
}
|
}
|
||||||
@@ -177,6 +181,9 @@ func nodeRecordFromModel(n *model.Node) *model.NodeRecord {
|
|||||||
if n.ServerIPV6.Valid {
|
if n.ServerIPV6.Valid {
|
||||||
rec.ServerIPv6 = strings.TrimSpace(n.ServerIPV6.String)
|
rec.ServerIPv6 = strings.TrimSpace(n.ServerIPV6.String)
|
||||||
}
|
}
|
||||||
|
if n.ExtraIPs.Valid {
|
||||||
|
rec.ExtraIPs = strings.TrimSpace(n.ExtraIPs.String)
|
||||||
|
}
|
||||||
if n.InterfaceName.Valid {
|
if n.InterfaceName.Valid {
|
||||||
rec.InterfaceName = strings.TrimSpace(n.InterfaceName.String)
|
rec.InterfaceName = strings.TrimSpace(n.InterfaceName.String)
|
||||||
}
|
}
|
||||||
@@ -289,10 +296,11 @@ func (r *Repository) ListChainNodesForTunnel(tunnelID int64) ([]model.ChainNodeR
|
|||||||
Name sql.NullString
|
Name sql.NullString
|
||||||
Protocol sql.NullString
|
Protocol sql.NullString
|
||||||
Strategy sql.NullString
|
Strategy sql.NullString
|
||||||
|
ConnectIP sql.NullString
|
||||||
}
|
}
|
||||||
var rows []row
|
var rows []row
|
||||||
err := r.db.Model(&model.ChainTunnel{}).
|
err := r.db.Model(&model.ChainTunnel{}).
|
||||||
Select("chain_tunnel.chain_type, chain_tunnel.inx, chain_tunnel.node_id, chain_tunnel.port, node.name, chain_tunnel.protocol, chain_tunnel.strategy").
|
Select("chain_tunnel.chain_type, chain_tunnel.inx, chain_tunnel.node_id, chain_tunnel.port, node.name, chain_tunnel.protocol, chain_tunnel.strategy, chain_tunnel.connect_ip").
|
||||||
Joins("LEFT JOIN node ON node.id = chain_tunnel.node_id").
|
Joins("LEFT JOIN node ON node.id = chain_tunnel.node_id").
|
||||||
Where("chain_tunnel.tunnel_id = ?", tunnelID).
|
Where("chain_tunnel.tunnel_id = ?", tunnelID).
|
||||||
Order("chain_tunnel.chain_type ASC, chain_tunnel.inx ASC, chain_tunnel.id ASC").
|
Order("chain_tunnel.chain_type ASC, chain_tunnel.inx ASC, chain_tunnel.id ASC").
|
||||||
@@ -337,6 +345,9 @@ func (r *Repository) ListChainNodesForTunnel(tunnelID int64) ([]model.ChainNodeR
|
|||||||
} else {
|
} else {
|
||||||
item.Strategy = row.Strategy.String
|
item.Strategy = row.Strategy.String
|
||||||
}
|
}
|
||||||
|
if row.ConnectIP.Valid {
|
||||||
|
item.ConnectIP = row.ConnectIP.String
|
||||||
|
}
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ func (r *Repository) GetUserDefaultsForTunnel(userID int64) (flow int64, num int
|
|||||||
return user.Flow, user.Num, user.ExpTime, user.FlowResetTime, nil
|
return user.Flow, user.Num, user.ExpTime, user.FlowResetTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig interface{}) error {
|
func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serverIPV6, port, interfaceName, version interface{}, httpFlag, tlsFlag, socksFlag int, now int64, status int, tcpAddr, udpAddr string, inx, isRemote int, remoteURL, remoteToken, remoteConfig, extraIPs interface{}) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
@@ -206,6 +206,7 @@ func (r *Repository) CreateNode(name, secret, serverIP string, serverIPV4, serve
|
|||||||
ServerIP: serverIP,
|
ServerIP: serverIP,
|
||||||
ServerIPV4: nullStringFromInterface(serverIPV4),
|
ServerIPV4: nullStringFromInterface(serverIPV4),
|
||||||
ServerIPV6: nullStringFromInterface(serverIPV6),
|
ServerIPV6: nullStringFromInterface(serverIPV6),
|
||||||
|
ExtraIPs: nullStringFromInterface(extraIPs),
|
||||||
Port: stringFromInterface(port),
|
Port: stringFromInterface(port),
|
||||||
InterfaceName: nullStringFromInterface(interfaceName),
|
InterfaceName: nullStringFromInterface(interfaceName),
|
||||||
Version: nullStringFromInterface(version),
|
Version: nullStringFromInterface(version),
|
||||||
@@ -238,7 +239,7 @@ func (r *Repository) GetNodeStatusFields(nodeID int64) (status, httpFlag, tlsFla
|
|||||||
return node.Status, node.HTTP, node.TLS, node.Socks, nil
|
return node.Status, node.HTTP, node.TLS, node.Socks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, serverIPV6, port, interfaceName, extraIPs interface{}, httpFlag, tlsFlag, socksFlag int, tcpAddr, udpAddr string, now int64) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
@@ -249,6 +250,7 @@ func (r *Repository) UpdateNode(id int64, name, serverIP string, serverIPV4, ser
|
|||||||
"server_ip": serverIP,
|
"server_ip": serverIP,
|
||||||
"server_ip_v4": nullStringFromInterface(serverIPV4),
|
"server_ip_v4": nullStringFromInterface(serverIPV4),
|
||||||
"server_ip_v6": nullStringFromInterface(serverIPV6),
|
"server_ip_v6": nullStringFromInterface(serverIPV6),
|
||||||
|
"extra_ips": nullStringFromInterface(extraIPs),
|
||||||
"port": stringFromInterface(port),
|
"port": stringFromInterface(port),
|
||||||
"interface_name": nullStringFromInterface(interfaceName),
|
"interface_name": nullStringFromInterface(interfaceName),
|
||||||
"http": httpFlag,
|
"http": httpFlag,
|
||||||
@@ -395,7 +397,7 @@ func (r *Repository) DeleteChainTunnelsByTunnelTx(tx *gorm.DB, tunnelID int64) e
|
|||||||
return tx.Where("tunnel_id = ?", tunnelID).Delete(&model.ChainTunnel{}).Error
|
return tx.Where("tunnel_id = ?", tunnelID).Delete(&model.ChainTunnel{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateChainTunnelTx(tx *gorm.DB, tunnelID int64, chainType string, nodeID int64, port sql.NullInt64, strategy string, inx int, protocol string) error {
|
func (r *Repository) CreateChainTunnelTx(tx *gorm.DB, tunnelID int64, chainType string, nodeID int64, port sql.NullInt64, strategy string, inx int, protocol string, connectIp string) error {
|
||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("database unavailable")
|
return errors.New("database unavailable")
|
||||||
}
|
}
|
||||||
@@ -407,6 +409,7 @@ func (r *Repository) CreateChainTunnelTx(tx *gorm.DB, tunnelID int64, chainType
|
|||||||
Strategy: nullStringFromInterface(strategy),
|
Strategy: nullStringFromInterface(strategy),
|
||||||
Inx: nullInt64FromInterface(inx),
|
Inx: nullInt64FromInterface(inx),
|
||||||
Protocol: nullStringFromInterface(protocol),
|
Protocol: nullStringFromInterface(protocol),
|
||||||
|
ConnectIP: sql.NullString{String: connectIp, Valid: connectIp != ""},
|
||||||
}
|
}
|
||||||
return tx.Create(&ct).Error
|
return tx.Create(&ct).Error
|
||||||
}
|
}
|
||||||
@@ -692,6 +695,7 @@ func (r *Repository) DeleteForwardCascade(forwardID int64) error {
|
|||||||
func (r *Repository) ReplaceForwardPorts(forwardID int64, entries []struct {
|
func (r *Repository) ReplaceForwardPorts(forwardID int64, entries []struct {
|
||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
|
InIP string
|
||||||
}) error {
|
}) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
@@ -705,7 +709,12 @@ func (r *Repository) ReplaceForwardPorts(forwardID int64, entries []struct {
|
|||||||
}
|
}
|
||||||
rows := make([]model.ForwardPort, 0, len(entries))
|
rows := make([]model.ForwardPort, 0, len(entries))
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
rows = append(rows, model.ForwardPort{ForwardID: forwardID, NodeID: e.NodeID, Port: e.Port})
|
rows = append(rows, model.ForwardPort{
|
||||||
|
ForwardID: forwardID,
|
||||||
|
NodeID: e.NodeID,
|
||||||
|
Port: e.Port,
|
||||||
|
InIP: sql.NullString{String: e.InIP, Valid: e.InIP != ""},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return tx.Create(&rows).Error
|
return tx.Create(&rows).Error
|
||||||
})
|
})
|
||||||
@@ -1168,7 +1177,7 @@ func (r *Repository) EnsureUserTunnelGrant(userID, tunnelID int64) (int64, bool,
|
|||||||
return ut.ID, true, nil
|
return ut.ID, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, speedID interface{}) (int64, error) {
|
func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, inIp string, speedID interface{}) (int64, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return 0, errors.New("repository not initialized")
|
return 0, errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
@@ -1198,6 +1207,7 @@ func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnel
|
|||||||
ForwardID: forwardID,
|
ForwardID: forwardID,
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
InIP: sql.NullString{String: inIp, Valid: inIp != ""},
|
||||||
}
|
}
|
||||||
if err := tx.Create(&fp).Error; err != nil {
|
if err := tx.Create(&fp).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ interface ForwardForm {
|
|||||||
name: string;
|
name: string;
|
||||||
tunnelId: number | null;
|
tunnelId: number | null;
|
||||||
inPort: number | null;
|
inPort: number | null;
|
||||||
|
inIp: string;
|
||||||
remoteAddr: string;
|
remoteAddr: string;
|
||||||
interfaceName?: string;
|
interfaceName?: string;
|
||||||
strategy: string;
|
strategy: string;
|
||||||
@@ -546,6 +547,7 @@ export default function ForwardPage() {
|
|||||||
name: "",
|
name: "",
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
|
inIp: "",
|
||||||
remoteAddr: "",
|
remoteAddr: "",
|
||||||
interfaceName: "",
|
interfaceName: "",
|
||||||
strategy: "fifo",
|
strategy: "fifo",
|
||||||
@@ -1164,6 +1166,7 @@ export default function ForwardPage() {
|
|||||||
name: "",
|
name: "",
|
||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
|
inIp: "",
|
||||||
remoteAddr: "",
|
remoteAddr: "",
|
||||||
interfaceName: "",
|
interfaceName: "",
|
||||||
strategy: "fifo",
|
strategy: "fifo",
|
||||||
@@ -1182,6 +1185,7 @@ export default function ForwardPage() {
|
|||||||
name: forward.name,
|
name: forward.name,
|
||||||
tunnelId: forward.tunnelId,
|
tunnelId: forward.tunnelId,
|
||||||
inPort: forward.inPort,
|
inPort: forward.inPort,
|
||||||
|
inIp: forward.inIp || "",
|
||||||
remoteAddr: forward.remoteAddr.split(",").join("\n"),
|
remoteAddr: forward.remoteAddr.split(",").join("\n"),
|
||||||
interfaceName: forward.interfaceName || "",
|
interfaceName: forward.interfaceName || "",
|
||||||
strategy: forward.strategy || "fifo",
|
strategy: forward.strategy || "fifo",
|
||||||
@@ -1263,6 +1267,7 @@ export default function ForwardPage() {
|
|||||||
name: form.name,
|
name: form.name,
|
||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
|
inIp: form.inIp || null,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||||
speedId: normalizeSpeedId(form.speedId),
|
speedId: normalizeSpeedId(form.speedId),
|
||||||
@@ -1270,11 +1275,11 @@ export default function ForwardPage() {
|
|||||||
|
|
||||||
res = await updateForward(updateData);
|
res = await updateForward(updateData);
|
||||||
} else {
|
} else {
|
||||||
// 创建时不需要id和userId(后端会自动设置)
|
|
||||||
const createData = {
|
const createData = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
|
inIp: form.inIp || null,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : "fifo",
|
strategy: addressCount > 1 ? form.strategy : "fifo",
|
||||||
speedId: normalizeSpeedId(form.speedId),
|
speedId: normalizeSpeedId(form.speedId),
|
||||||
@@ -4023,6 +4028,17 @@ export default function ForwardPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
description="留空使用默认入口IP,多IP节点可指定监听地址"
|
||||||
|
label="监听IP"
|
||||||
|
placeholder="留空使用默认"
|
||||||
|
value={form.inIp}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({ ...prev, inIp: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
description="格式: IP:端口 或 域名:端口,支持多个地址(每行一个)"
|
description="格式: IP:端口 或 域名:端口,支持多个地址(每行一个)"
|
||||||
errorMessage={errors.remoteAddr}
|
errorMessage={errors.remoteAddr}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ interface Node {
|
|||||||
port: string;
|
port: string;
|
||||||
tcpListenAddr?: string;
|
tcpListenAddr?: string;
|
||||||
udpListenAddr?: string;
|
udpListenAddr?: string;
|
||||||
|
extraIPs?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
http?: number; // 0 关 1 开
|
http?: number; // 0 关 1 开
|
||||||
tls?: number; // 0 关 1 开
|
tls?: number; // 0 关 1 开
|
||||||
@@ -99,7 +100,7 @@ interface Node {
|
|||||||
rollbackLoading?: boolean;
|
rollbackLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NodeForm {
|
interface NodeForm {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
name: string;
|
name: string;
|
||||||
serverHost: string;
|
serverHost: string;
|
||||||
@@ -109,6 +110,7 @@ interface NodeForm {
|
|||||||
tcpListenAddr: string;
|
tcpListenAddr: string;
|
||||||
udpListenAddr: string;
|
udpListenAddr: string;
|
||||||
interfaceName: string;
|
interfaceName: string;
|
||||||
|
extraIPs: string;
|
||||||
http: number; // 0 关 1 开
|
http: number; // 0 关 1 开
|
||||||
tls: number; // 0 关 1 开
|
tls: number; // 0 关 1 开
|
||||||
socks: number; // 0 关 1 开
|
socks: number; // 0 关 1 开
|
||||||
@@ -183,6 +185,7 @@ export default function NodePage() {
|
|||||||
tcpListenAddr: "[::]",
|
tcpListenAddr: "[::]",
|
||||||
udpListenAddr: "[::]",
|
udpListenAddr: "[::]",
|
||||||
interfaceName: "",
|
interfaceName: "",
|
||||||
|
extraIPs: "",
|
||||||
http: 0,
|
http: 0,
|
||||||
tls: 0,
|
tls: 0,
|
||||||
socks: 0,
|
socks: 0,
|
||||||
@@ -590,6 +593,7 @@ export default function NodePage() {
|
|||||||
tcpListenAddr: node.tcpListenAddr || "[::]",
|
tcpListenAddr: node.tcpListenAddr || "[::]",
|
||||||
udpListenAddr: node.udpListenAddr || "[::]",
|
udpListenAddr: node.udpListenAddr || "[::]",
|
||||||
interfaceName: (node as any).interfaceName || "",
|
interfaceName: (node as any).interfaceName || "",
|
||||||
|
extraIPs: node.extraIPs || "",
|
||||||
http: typeof node.http === "number" ? node.http : 1,
|
http: typeof node.http === "number" ? node.http : 1,
|
||||||
tls: typeof node.tls === "number" ? node.tls : 1,
|
tls: typeof node.tls === "number" ? node.tls : 1,
|
||||||
socks: typeof node.socks === "number" ? node.socks : 1,
|
socks: typeof node.socks === "number" ? node.socks : 1,
|
||||||
@@ -817,6 +821,7 @@ export default function NodePage() {
|
|||||||
const { serverHost, ...rest } = form;
|
const { serverHost, ...rest } = form;
|
||||||
const data = {
|
const data = {
|
||||||
...rest,
|
...rest,
|
||||||
|
extraIPs: form.extraIPs,
|
||||||
serverIp:
|
serverIp:
|
||||||
form.serverIpV4?.trim() ||
|
form.serverIpV4?.trim() ||
|
||||||
form.serverIpV6?.trim() ||
|
form.serverIpV6?.trim() ||
|
||||||
@@ -880,6 +885,7 @@ export default function NodePage() {
|
|||||||
tcpListenAddr: "[::]",
|
tcpListenAddr: "[::]",
|
||||||
udpListenAddr: "[::]",
|
udpListenAddr: "[::]",
|
||||||
interfaceName: "",
|
interfaceName: "",
|
||||||
|
extraIPs: "",
|
||||||
http: 0,
|
http: 0,
|
||||||
tls: 0,
|
tls: 0,
|
||||||
socks: 0,
|
socks: 0,
|
||||||
@@ -1637,6 +1643,17 @@ export default function NodePage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
description="多IP服务器可填写额外IP地址,逗号分隔"
|
||||||
|
label="额外IP地址"
|
||||||
|
placeholder="例如: 192.168.1.100, 10.0.0.5"
|
||||||
|
value={form.extraIPs}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((prev) => ({ ...prev, extraIPs: e.target.value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
errorMessage={errors.tcpListenAddr}
|
errorMessage={errors.tcpListenAddr}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ interface ChainTunnel {
|
|||||||
strategy?: string; // 'fifo' | 'round' | 'rand' - 仅转发链需要
|
strategy?: string; // 'fifo' | 'round' | 'rand' - 仅转发链需要
|
||||||
chainType?: number; // 1: 入口, 2: 转发链, 3: 出口
|
chainType?: number; // 1: 入口, 2: 转发链, 3: 出口
|
||||||
inx?: number; // 转发链序号
|
inx?: number; // 转发链序号
|
||||||
|
connectIp?: string; // 连接IP(多IP节点指定连接地址)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Tunnel {
|
interface Tunnel {
|
||||||
@@ -343,6 +344,20 @@ export default function TunnelPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新某一跳的所有节点的连接IP
|
||||||
|
const updateChainConnectIp = (groupIndex: number, connectIp: string) => {
|
||||||
|
setForm((prev) => {
|
||||||
|
const chainNodes = [...(prev.chainNodes || [])];
|
||||||
|
|
||||||
|
chainNodes[groupIndex] = (chainNodes[groupIndex] || []).map((node) => ({
|
||||||
|
...node,
|
||||||
|
connectIp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { ...prev, chainNodes };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// 获取所有转发链中已选择的节点ID列表
|
// 获取所有转发链中已选择的节点ID列表
|
||||||
const getSelectedChainNodeIds = (): number[] => {
|
const getSelectedChainNodeIds = (): number[] => {
|
||||||
return (form.chainNodes || []).flatMap((group) =>
|
return (form.chainNodes || []).flatMap((group) =>
|
||||||
@@ -1741,6 +1756,29 @@ export default function TunnelPage() {
|
|||||||
<SelectItem key="rand">随机</SelectItem>
|
<SelectItem key="rand">随机</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 连接IP - 转发链节点 */}
|
||||||
|
<Input
|
||||||
|
classNames={{
|
||||||
|
label: "text-xs",
|
||||||
|
}}
|
||||||
|
description="多IP节点可指定连接地址,留空使用默认"
|
||||||
|
label="连接IP"
|
||||||
|
placeholder="留空使用默认"
|
||||||
|
size="sm"
|
||||||
|
value={
|
||||||
|
groupNodes.length > 0
|
||||||
|
? groupNodes[0].connectIp || ""
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e) => {
|
||||||
|
updateChainConnectIp(
|
||||||
|
groupIndex,
|
||||||
|
e.target.value,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -2002,6 +2040,50 @@ export default function TunnelPage() {
|
|||||||
<SelectItem key="rand">随机</SelectItem>
|
<SelectItem key="rand">随机</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 连接IP - 出口节点 */}
|
||||||
|
<Input
|
||||||
|
classNames={{
|
||||||
|
label: "text-xs",
|
||||||
|
}}
|
||||||
|
description="多IP节点可指定连接地址,留空使用默认"
|
||||||
|
label="连接IP"
|
||||||
|
placeholder="留空使用默认"
|
||||||
|
size="sm"
|
||||||
|
value={
|
||||||
|
form.outNodeId && form.outNodeId.length > 0
|
||||||
|
? form.outNodeId[0].connectIp || ""
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
variant="bordered"
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setForm((prev) => {
|
||||||
|
const currentOutNodes = prev.outNodeId || [];
|
||||||
|
if (currentOutNodes.length === 0) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: [
|
||||||
|
{
|
||||||
|
nodeId: -1,
|
||||||
|
chainType: 3,
|
||||||
|
protocol: "tls",
|
||||||
|
strategy: "round",
|
||||||
|
connectIp: value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: currentOutNodes.map((ct) => ({
|
||||||
|
...ct,
|
||||||
|
connectIp: value,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user