diff --git a/go-backend/MIGRATION_PLAN.md b/go-backend/MIGRATION_PLAN.md index baf3c8e..5a9fe10 100644 --- a/go-backend/MIGRATION_PLAN.md +++ b/go-backend/MIGRATION_PLAN.md @@ -90,7 +90,7 @@ | 表名 | Model | 特殊处理 | |------|-------|----------| | `user` | `User` | `TableName()` 返回 `"user"` (PG 保留字) | -| `forward` | `Forward` | 增加 `proxy_protocol` 字段 | +| `forward` | `Forward` | | | `forward_port` | `ForwardPort` | | | `node` | `Node` | | | `speed_limit` | `SpeedLimit` | | diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 9cc5479..e4f8d1e 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -1705,12 +1705,6 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel if cLimiterName != "" { service["climiter"] = cLimiterName } - if forward.ProxyProtocol > 0 { - if service["metadata"] == nil { - service["metadata"] = map[string]interface{}{} - } - service["metadata"].(map[string]interface{})["proxyProtocol"] = forward.ProxyProtocol - } if protocol == "udp" { listenerMetadata := map[string]interface{}{ "keepAlive": true, @@ -1722,10 +1716,7 @@ func buildForwardServiceConfigs(baseName string, forward *forwardRecord, tunnel service["handler"].(map[string]interface{})["chain"] = fmt.Sprintf("chains_%d", forward.TunnelID) } if tunnel != nil && tunnel.Type == 1 && strings.TrimSpace(node.InterfaceName) != "" { - if service["metadata"] == nil { - service["metadata"] = map[string]interface{}{} - } - service["metadata"].(map[string]interface{})["interface"] = node.InterfaceName + service["metadata"] = map[string]interface{}{"interface": node.InterfaceName} } if limiterID != nil && *limiterID > 0 { service["limiter"] = strconv.FormatInt(*limiterID, 10) diff --git a/go-backend/internal/http/handler/forward_proxy_protocol_test.go b/go-backend/internal/http/handler/forward_proxy_protocol_test.go deleted file mode 100644 index dacaa17..0000000 --- a/go-backend/internal/http/handler/forward_proxy_protocol_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package handler - -import ( - "testing" - "time" - - "go-backend/internal/store/model" - "go-backend/internal/store/repo" -) - -func TestBuildForwardServiceConfigsPreservesProxyProtocolWithInterfaceMetadata(t *testing.T) { - forward := &forwardRecord{ - ID: 1, - UserID: 2, - TunnelID: 3, - RemoteAddr: "1.1.1.1:443", - Strategy: "fifo", - ProxyProtocol: 2, - } - tunnel := &tunnelRecord{Type: 1} - node := &nodeRecord{ - InterfaceName: "eth0", - TCPListenAddr: "0.0.0.0", - UDPListenAddr: "0.0.0.0", - } - - services := buildForwardServiceConfigs("1_2_3", forward, tunnel, node, 4001, "", nil, "") - if len(services) != 2 { - t.Fatalf("expected 2 services, got %d", len(services)) - } - - for _, service := range services { - metadata, ok := service["metadata"].(map[string]interface{}) - if !ok { - t.Fatalf("expected metadata map, got %T", service["metadata"]) - } - if metadata["interface"] != "eth0" { - t.Fatalf("expected interface metadata eth0, got %v", metadata["interface"]) - } - if metadata["proxyProtocol"] != 2 { - t.Fatalf("expected proxyProtocol 2, got %v", metadata["proxyProtocol"]) - } - } -} - -func TestRollbackForwardMutationRestoresProxyProtocol(t *testing.T) { - r, err := repo.Open(":memory:") - if err != nil { - t.Fatalf("open repo: %v", err) - } - defer r.Close() - - now := time.Now().UnixMilli() - if err := r.DB().Create(&model.Forward{ - UserID: 2, - UserName: "rollback-user", - Name: "rollback-forward", - TunnelID: 3, - RemoteAddr: "9.9.9.9:443", - Strategy: "fifo", - CreatedTime: now, - UpdatedTime: now, - Status: 1, - ProxyProtocol: 2, - }).Error; err != nil { - t.Fatalf("create forward: %v", err) - } - - forwardID := mustLastInsertID(t, r, "rollback-forward") - if err := r.DB().Model(&model.Forward{}).Where("id = ?", forwardID).Updates(map[string]interface{}{ - "name": "changed-forward", - "proxy_protocol": 0, - "updated_time": now + 1, - }).Error; err != nil { - t.Fatalf("mutate forward: %v", err) - } - - h := &Handler{repo: r} - h.rollbackForwardMutation(&forwardRecord{ - ID: forwardID, - UserID: 2, - UserName: "rollback-user", - Name: "rollback-forward", - TunnelID: 3, - RemoteAddr: "9.9.9.9:443", - Strategy: "fifo", - Status: 1, - ProxyProtocol: 2, - }, nil) - - var proxyProtocol int - if err := r.DB().Raw("SELECT proxy_protocol FROM forward WHERE id = ?", forwardID).Row().Scan(&proxyProtocol); err != nil { - t.Fatalf("query proxy_protocol: %v", err) - } - if proxyProtocol != 2 { - t.Fatalf("expected proxyProtocol restored to 2, got %d", proxyProtocol) - } -} diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 2c5f112..9324737 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -1728,7 +1728,7 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) { } if roleID != 0 { if err := IsSafeRemoteAddr(remoteAddr); err != nil { - response.WriteJSON(w, response.Err(403, err.Error())) + response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络")) return } if speedIDVal, ok := req["speedId"]; ok && speedIDVal != nil { @@ -1780,9 +1780,7 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) { userName = "user" } maxConn := asInt(req["maxConn"], 0) - proxyProtocol := asInt(req["proxyProtocol"], 0) - - forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, proxyProtocol) + forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn) if err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return @@ -1855,7 +1853,7 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) { } if actorRole != 0 { if err := IsSafeRemoteAddr(remoteAddr); err != nil { - response.WriteJSON(w, response.Err(403, err.Error())) + response.WriteJSON(w, response.Err(403, "禁止将目标地址设置为内部网络")) return } } @@ -1934,9 +1932,8 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) { } now := time.Now().UnixMilli() maxConn := asInt(req["maxConn"], forward.MaxConn) - proxyProtocol := asInt(req["proxyProtocol"], forward.ProxyProtocol) - if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn, proxyProtocol); err != nil { + if err := h.repo.UpdateForward(id, name, tunnelID, remoteAddr, strategy, now, newSpeedID, maxConn); err != nil { response.WriteJSON(w, response.Err(-2, err.Error())) return } @@ -4088,7 +4085,7 @@ func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts [] h.repo.RollbackForwardFields( oldForward.ID, oldForward.UserID, oldForward.UserName, oldForward.Name, oldForward.TunnelID, oldForward.RemoteAddr, oldForward.Strategy, oldForward.Status, - oldForward.SpeedID, oldForward.MaxConn, oldForward.ProxyProtocol, + oldForward.SpeedID, oldForward.MaxConn, time.Now().UnixMilli(), ) diff --git a/go-backend/internal/http/handler/security_utils.go b/go-backend/internal/http/handler/security_utils.go index 3516729..7132fa7 100644 --- a/go-backend/internal/http/handler/security_utils.go +++ b/go-backend/internal/http/handler/security_utils.go @@ -11,37 +11,11 @@ var DisableSafeRemoteAddrCheckForTesting = false // IsSafeRemoteAddr checks if a given address is safe to connect to (prevents SSRF/Open Proxy). // It resolves domains to IPs to prevent DNS rebinding attacks pointing to internal networks. -// Supports multiple addresses separated by commas or newlines (one per line). func IsSafeRemoteAddr(addr string) error { if DisableSafeRemoteAddrCheckForTesting { return nil } - for _, part := range splitRemoteParts(addr) { - if err := checkSingleRemoteAddr(part); err != nil { - return err - } - } - return nil -} - -// splitRemoteParts splits a multi-address string by commas and newlines. -func splitRemoteParts(addr string) []string { - addr = strings.ReplaceAll(addr, "\n", ",") - addr = strings.ReplaceAll(addr, "\r", ",") - parts := strings.Split(addr, ",") - out := make([]string, 0, len(parts)) - for _, part := range parts { - part = strings.TrimSpace(part) - if part != "" { - out = append(out, part) - } - } - return out -} - -// checkSingleRemoteAddr validates a single address. -func checkSingleRemoteAddr(addr string) error { host, _, err := net.SplitHostPort(addr) if err != nil { if strings.Contains(err.Error(), "missing port in address") { @@ -53,12 +27,12 @@ func checkSingleRemoteAddr(addr string) error { ips, err := net.LookupIP(host) if err != nil { - return fmt.Errorf("could not resolve address %q: %v", addr, err) + return fmt.Errorf("could not resolve address: %v", err) } for _, ip := range ips { if ip.IsLoopback() || ip.IsPrivate() { - return fmt.Errorf("address %q resolves to internal IP: %s", addr, ip.String()) + return fmt.Errorf("address resolves to internal IP: %s", ip.String()) } } diff --git a/go-backend/internal/http/handler/upgrade.go b/go-backend/internal/http/handler/upgrade.go index bf203f1..ebde530 100644 --- a/go-backend/internal/http/handler/upgrade.go +++ b/go-backend/internal/http/handler/upgrade.go @@ -13,13 +13,6 @@ import ( "go-backend/internal/http/response" ) -// failedForward tracks a forward that failed redeployment, for retry. -type failedForward struct { - id int64 - forward *forwardRecord - err error -} - const ( githubRepo = "Sagit-chu/flvx" githubAPIBase = "https://api.github.com" @@ -397,9 +390,6 @@ func (h *Handler) consumeNodePendingUpgradeRedeploy(nodeID int64) bool { func (h *Handler) onNodeOnline(nodeID int64) { h.consumeNodePendingUpgradeRedeploy(nodeID) - // Always redeploy rules on reconnection, not just for pending upgrade nodes. - // This handles cases where the node restarted and lost its in-memory config - // before persistence had time to flush, or if the panel also restarted. h.redeployNodeRuntimeAfterUpgrade(nodeID) } @@ -415,7 +405,6 @@ func (h *Handler) redeployNodeRuntimeAfterUpgrade(nodeID int64) { return } - // First pass: deploy everything tunnelFailed := make(map[int64]struct{}) for _, tunnelID := range tunnelIDs { if err := h.redeployTunnelAndForwards(tunnelID); err != nil { @@ -424,9 +413,6 @@ func (h *Handler) redeployNodeRuntimeAfterUpgrade(nodeID int64) { } } - // Collect forwards that failed independently (not skipped due to tunnel failure) - var failedForwards []failedForward - for _, forwardID := range forwardIDs { forward, getErr := h.getForwardRecord(forwardID) if getErr != nil || forward == nil { @@ -436,86 +422,7 @@ func (h *Handler) redeployNodeRuntimeAfterUpgrade(nodeID int64) { continue } if err := h.syncForwardServices(forward, "UpdateService", true); err != nil { - failedForwards = append(failedForwards, failedForward{id: forwardID, forward: forward, err: err}) fmt.Printf("post-upgrade redeploy: forward %d failed on node %d: %v\n", forwardID, nodeID, err) } } - - // Retry failed items with exponential backoff (max 3 attempts) - h.retryFailedRedeploys(nodeID, tunnelFailed, failedForwards) -} - -// isRetryableError returns true if the error looks transient and worth retrying. -func isRetryableError(err error) bool { - if err == nil { - return false - } - msg := strings.ToLower(err.Error()) - // Skip non-retryable errors: not-found, already-exists, validation errors - if strings.Contains(msg, "not found") || strings.Contains(msg, "不存在") { - return false - } - if strings.Contains(msg, "already exists") || strings.Contains(msg, "已存在") { - return false - } - // Everything else (timeout, connection lost, port in use, etc.) is retryable - return true -} - -// retryFailedRedeploys retries failed tunnels and forwards with exponential backoff. -func (h *Handler) retryFailedRedeploys(nodeID int64, tunnelFailed map[int64]struct{}, failedForwards []failedForward) { - if len(tunnelFailed) == 0 && len(failedForwards) == 0 { - return - } - - const maxRetries = 3 - baseDelay := time.Second - - for attempt := 1; attempt <= maxRetries; attempt++ { - delay := baseDelay * time.Duration(1< { const isH5 = useH5Mode(); - const authenticated = isLoggedIn(); + const navigate = useNavigate(); + const [authenticated, setAuthenticated] = useState(() => isLoggedIn()); + + useEffect(() => { + const handleSessionChange = () => { + const loggedIn = isLoggedIn(); + + setAuthenticated(loggedIn); + + if (!loggedIn) { + navigate("/", { replace: true }); + } + }; + + window.addEventListener(SESSION_UPDATED_EVENT, handleSessionChange); + + return () => { + window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionChange); + }; + }, [navigate]); + + useEffect(() => { + if (!authenticated) { + navigate("/", { replace: true }); + } + }, [authenticated, navigate]); if (!authenticated) { return ; @@ -78,22 +103,6 @@ const LoginRoute = () => { function App() { const location = useLocation(); - const navigate = useNavigate(); - - // 全局登录状态监听,当检测到未登录且不在首页时,跳转到首页 - useEffect(() => { - const handleSessionUpdate = () => { - if (!isLoggedIn() && location.pathname !== "/") { - navigate("/", { replace: true }); - } - }; - - window.addEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate); - - return () => { - window.removeEventListener(SESSION_UPDATED_EVENT, handleSessionUpdate); - }; - }, [location.pathname, navigate]); // 处理自定义背景图片 useEffect(() => { diff --git a/vite-frontend/src/api/types.ts b/vite-frontend/src/api/types.ts index cd3c5f0..dc31da5 100644 --- a/vite-frontend/src/api/types.ts +++ b/vite-frontend/src/api/types.ts @@ -71,8 +71,6 @@ export interface ForwardApiItem { userId?: number; tunnelId?: number; speedId?: number | null; - maxConn?: number; - proxyProtocol?: number; inx?: number; [key: string]: unknown; } @@ -203,7 +201,7 @@ export interface UserPackageInfoApiData { num: number; expTime?: string; flowResetTime?: number; - maxConn?: number; + maxConn?: number; [key: string]: unknown; }; tunnelPermissions: UserTunnelPermissionApiItem[]; @@ -372,8 +370,6 @@ export interface ForwardMutationPayload { remoteAddr?: string; strategy?: string; speedId?: number | null; - maxConn?: number; - proxyProtocol?: number; } export interface SpeedLimitMutationPayload { diff --git a/vite-frontend/src/layouts/admin.tsx b/vite-frontend/src/layouts/admin.tsx index ed68d58..ee7d7d8 100644 --- a/vite-frontend/src/layouts/admin.tsx +++ b/vite-frontend/src/layouts/admin.tsx @@ -241,6 +241,7 @@ export default function AdminLayout({ // 退出登录 const handleLogout = () => { safeLogout(); + navigate("/"); }; // 切换移动端菜单 diff --git a/vite-frontend/src/pages/change-password.tsx b/vite-frontend/src/pages/change-password.tsx index 4de6dd9..2d62009 100644 --- a/vite-frontend/src/pages/change-password.tsx +++ b/vite-frontend/src/pages/change-password.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import toast from "react-hot-toast"; import { Button } from "@/shadcn-bridge/heroui/button"; @@ -25,6 +26,7 @@ export default function ChangePasswordPage() { }); const [loading, setLoading] = useState(false); const [errors, setErrors] = useState>({}); + const navigate = useNavigate(); const validateForm = (): boolean => { const newErrors: Partial = {}; @@ -96,6 +98,7 @@ export default function ChangePasswordPage() { const logout = () => { safeLogout(); + navigate("/"); }; const handleKeyPress = (e: React.KeyboardEvent) => { diff --git a/vite-frontend/src/pages/dashboard.tsx b/vite-frontend/src/pages/dashboard.tsx index 5b63697..5c6c12c 100644 --- a/vite-frontend/src/pages/dashboard.tsx +++ b/vite-frontend/src/pages/dashboard.tsx @@ -50,6 +50,7 @@ export default function DashboardPage() { const handleLogout = () => { safeLogout(); toast.success("已退出登录"); + navigate("/"); }; const { diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index d2f981b..4cb23ef 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -125,7 +125,6 @@ interface Forward { userId?: number; inx?: number; speedId?: number | null; - proxyProtocol?: number; } interface Tunnel { @@ -161,7 +160,6 @@ interface ForwardForm { strategy: string; speedId: number | null; maxConn?: number; - proxyProtocol?: number; } interface ForwardUserGroup { @@ -578,11 +576,6 @@ const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => { typeof forward.speedId === "number" || forward.speedId === null ? forward.speedId : undefined, - maxConn: typeof forward.maxConn === "number" ? forward.maxConn : undefined, - proxyProtocol: - typeof forward.proxyProtocol === "number" - ? forward.proxyProtocol - : undefined, serviceRunning: forward.status === 1, })); }; @@ -1317,7 +1310,6 @@ export default function ForwardPage() { strategy: "fifo", speedId: null, maxConn: 0, - proxyProtocol: 0, }); const [inIpTouched, setInIpTouched] = useState(false); @@ -2105,7 +2097,6 @@ export default function ForwardPage() { interfaceName: "", strategy: "fifo", speedId: null, - proxyProtocol: 0, }); setErrors({}); setModalOpen(true); @@ -2126,8 +2117,7 @@ export default function ForwardPage() { interfaceName: forward.interfaceName || "", strategy: forward.strategy || "fifo", speedId: normalizeSpeedId(forward.speedId), - maxConn: forward.maxConn ?? 0, - proxyProtocol: forward.proxyProtocol ?? 0, + maxConn: forward.maxConn || 0, }); setErrors({}); setModalOpen(true); @@ -2257,7 +2247,6 @@ export default function ForwardPage() { strategy: addressCount > 1 ? form.strategy : "fifo", speedId: normalizedSpeedId, maxConn: form.maxConn, - proxyProtocol: form.proxyProtocol, }; res = await updateForward(updateData); @@ -2271,11 +2260,11 @@ export default function ForwardPage() { strategy: addressCount > 1 ? form.strategy : "fifo", speedId: normalizedSpeedId, maxConn: form.maxConn, - proxyProtocol: form.proxyProtocol, }; res = await createForward(createData); } + if (res.code === 0) { const warningItems = Array.isArray((res as any).data?.warnings) ? (res as any).data.warnings @@ -4753,6 +4742,8 @@ export default function ForwardPage() { } /> + + )} - + - 高级设置 - - } + title={高级设置} >
{ - const value = Math.max( - Number(e.target.value) || 0, - 0, - ); - + const value = Math.max(Number(e.target.value) || 0, 0); setForm((prev) => ({ ...prev, maxConn: value })); }} - /> - + /> {isAdmin && ( { const value = Math.max(Number(e.target.value) || 0, 0); - setUserForm((prev) => ({ ...prev, maxConn: value })); }} /> diff --git a/vite-frontend/src/types/index.ts b/vite-frontend/src/types/index.ts index 3e07e15..6784911 100644 --- a/vite-frontend/src/types/index.ts +++ b/vite-frontend/src/types/index.ts @@ -24,7 +24,6 @@ export interface User { monthlyUsedBytes?: number; disabledByQuota?: number; quotaDisabledAt?: number; - maxConn?: number; } export interface UserGroup { diff --git a/vite-frontend/src/utils/logout.ts b/vite-frontend/src/utils/logout.ts index 397f40e..857da32 100644 --- a/vite-frontend/src/utils/logout.ts +++ b/vite-frontend/src/utils/logout.ts @@ -2,8 +2,9 @@ import { clearSession } from "@/utils/session"; /** * 安全退出登录函数 - * 清除登录相关数据 + * 清除登录相关数据,并强制刷新跳转到首页 */ export const safeLogout = () => { clearSession(); + window.location.href = "/"; };