From 5ddb46c3c253ea0fbc84d539b48511a2ccd1d664 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 02:25:16 +0000 Subject: [PATCH 1/6] docs: add caddy reverse proxy guide --- doc/install.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/doc/install.md b/doc/install.md index c76fbde..b6bd507 100644 --- a/doc/install.md +++ b/doc/install.md @@ -77,3 +77,57 @@ curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/install.sh - 安装完成后,服务会自动启动。 - 查看状态: `systemctl status flux_agent` - 回到面板 **节点管理** 页面,该节点状态应显示为 **在线**。 + +--- + +## 三、Caddy 反向代理(可选) + +如果需要通过域名访问面板并自动获取 HTTPS 证书,可以使用 Caddy 作为反向代理。 + +### 1. 安装 Caddy + +```bash +# Debian / Ubuntu +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl +curl -1sLf 'https://dl.cloudflare.com/content/v1/e2qwFJ2fRP2b2q/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudflare.com/content/v1/e2qwFJ2fRP2b2q/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list +sudo apt update +sudo apt install caddy +``` + +其他系统请参考 [Caddy 官方安装文档](https://caddyserver.com/docs/install)。 + +### 2. 配置 Caddyfile + +编辑 Caddy 配置文件: + +```bash +sudo nano /etc/caddy/Caddyfile +``` + +#### 面板域名配置 + +将 `panel.example.com` 替换为你自己的域名: + +```caddyfile +panel.example.com { + reverse_proxy localhost:6366 +} +``` + +Caddy 会自动为域名申请和续期 HTTPS 证书,无需额外配置。 + +### 3. 重启 Caddy + +```bash +sudo systemctl restart caddy +``` + +### 4. 注意事项 + +- 确保域名已正确解析到服务器 IP。 +- 确保服务器防火墙放行了 **80** 和 **443** 端口(Caddy 自动申请证书需要)。 +- 使用 Caddy 反向代理后,可以在 `.env` 中将前端端口改为仅监听本地,避免直接暴露: + ``` + FRONTEND_PORT=127.0.0.1:6366 + ``` From d6b83a0c1bec66f3d628519adaf610b54bdc0abe Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 02:48:57 +0000 Subject: [PATCH 2/6] feat(panel-sharing): add edit support for peer shares Allow editing port range, traffic limit, allowed domains, allowed API IPs and expiry time on existing shares via a new update endpoint and edit modal in the frontend. --- .../internal/http/handler/federation.go | 85 ++++++++++++ go-backend/internal/http/handler/handler.go | 1 + vite-frontend/src/api/index.ts | 10 ++ vite-frontend/src/pages/panel-sharing.tsx | 127 ++++++++++++++++++ 4 files changed, 223 insertions(+) diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index e3bea0d..4f6c26f 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -40,6 +40,17 @@ type resetPeerShareFlowRequest struct { ID int64 `json:"id"` } +type updatePeerShareRequest struct { + ID int64 `json:"id"` + Name string `json:"name"` + MaxBandwidth int64 `json:"maxBandwidth"` + ExpiryTime int64 `json:"expiryTime"` + PortRangeStart int `json:"portRangeStart"` + PortRangeEnd int `json:"portRangeEnd"` + AllowedDomains string `json:"allowedDomains"` + AllowedIPs string `json:"allowedIps"` +} + type nodeImportRequest struct { RemoteURL string `json:"remoteUrl"` Token string `json:"token"` @@ -325,6 +336,80 @@ func (h *Handler) federationShareResetFlow(w http.ResponseWriter, r *http.Reques response.WriteJSON(w, response.OKEmpty()) } +func (h *Handler) federationShareUpdate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("Invalid method")) + return + } + + var req updatePeerShareRequest + if err := decodeJSON(r.Body, &req); err != nil { + response.WriteJSON(w, response.ErrDefault("Invalid JSON")) + return + } + if req.ID <= 0 { + response.WriteJSON(w, response.ErrDefault("Share ID is required")) + return + } + + share, err := h.repo.GetPeerShare(req.ID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + if share == nil { + response.WriteJSON(w, response.ErrDefault("Share not found")) + return + } + + if req.Name == "" { + response.WriteJSON(w, response.ErrDefault("Name is required")) + return + } + + if req.MaxBandwidth < 0 { + response.WriteJSON(w, response.ErrDefault("Max bandwidth cannot be negative")) + return + } + + if req.ExpiryTime < 0 { + response.WriteJSON(w, response.ErrDefault("Expiry time cannot be negative")) + return + } + + if req.PortRangeStart < 0 || req.PortRangeStart > 65535 || req.PortRangeEnd < 0 || req.PortRangeEnd > 65535 { + response.WriteJSON(w, response.ErrDefault("Invalid port range")) + return + } + + if req.PortRangeStart > req.PortRangeEnd { + response.WriteJSON(w, response.ErrDefault("Port range start cannot be greater than end")) + return + } + + allowedIPs, err := normalizePeerShareAllowedIPs(req.AllowedIPs) + if err != nil { + response.WriteJSON(w, response.ErrDefault(err.Error())) + return + } + + share.Name = req.Name + share.MaxBandwidth = req.MaxBandwidth + share.ExpiryTime = req.ExpiryTime + share.PortRangeStart = req.PortRangeStart + share.PortRangeEnd = req.PortRangeEnd + share.AllowedDomains = req.AllowedDomains + share.AllowedIPs = allowedIPs + share.UpdatedTime = time.Now().UnixMilli() + + if err := h.repo.UpdatePeerShare(share); err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + + response.WriteJSON(w, response.OKEmpty()) +} + func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index 4bfec49..a0ce67c 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -155,6 +155,7 @@ func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore) mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList) mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate) + mux.HandleFunc("/api/v1/federation/share/update", h.federationShareUpdate) mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete) mux.HandleFunc("/api/v1/federation/share/reset-flow", h.federationShareResetFlow) mux.HandleFunc("/api/v1/federation/share/remote-usage/list", h.federationRemoteUsageList) diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index f0b56d2..bd8c71e 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -200,6 +200,16 @@ export const createPeerShare = (data: { allowedDomains?: string; allowedIps?: string; }) => Network.post("/federation/share/create", data); +export const updatePeerShare = (data: { + id: number; + name: string; + maxBandwidth: number; + expiryTime: number; + portRangeStart: number; + portRangeEnd: number; + allowedDomains: string; + allowedIps: string; +}) => Network.post("/federation/share/update", data); export const deletePeerShare = (id: number) => Network.post("/federation/share/delete", { id }); export const resetPeerShareFlow = (id: number) => diff --git a/vite-frontend/src/pages/panel-sharing.tsx b/vite-frontend/src/pages/panel-sharing.tsx index fdd881c..2b5205a 100644 --- a/vite-frontend/src/pages/panel-sharing.tsx +++ b/vite-frontend/src/pages/panel-sharing.tsx @@ -20,6 +20,7 @@ import { resetPeerShareFlow, getPeerRemoteUsageList, importRemoteNode, + updatePeerShare, } from "@/api"; interface Node { @@ -91,6 +92,7 @@ export default function PanelSharingPage() { // Modals const [createShareOpen, setCreateShareOpen] = useState(false); + const [editShareOpen, setEditShareOpen] = useState(false); const [importNodeOpen, setImportNodeOpen] = useState(false); // Forms @@ -110,6 +112,17 @@ export default function PanelSharingPage() { token: "", }); + const [editForm, setEditForm] = useState({ + id: 0, + name: "", + maxBandwidth: 0, + expiryTime: 0, + portRangeStart: 10000, + portRangeEnd: 20000, + allowedDomains: "", + allowedIps: "", + }); + const loadShares = useCallback(async () => { setLoading(true); try { @@ -239,6 +252,52 @@ export default function PanelSharingPage() { } }; + const openEditShare = (share: PeerShare) => { + setEditForm({ + id: share.id, + name: share.name, + maxBandwidth: share.maxBandwidth > 0 ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024)) : 0, + expiryTime: share.expiryTime, + portRangeStart: share.portRangeStart, + portRangeEnd: share.portRangeEnd, + allowedDomains: share.allowedDomains || "", + allowedIps: share.allowedIps || "", + }); + setEditShareOpen(true); + }; + + const handleEditShare = async () => { + if (!editForm.name) { + toast.error("名称不能为空"); + return; + } + if (editForm.maxBandwidth < 0) { + toast.error("流量上限不能为负数"); + return; + } + try { + const res = await updatePeerShare({ + id: editForm.id, + name: editForm.name, + maxBandwidth: Math.max(0, editForm.maxBandwidth) * 1024 * 1024 * 1024, + expiryTime: editForm.expiryTime, + portRangeStart: editForm.portRangeStart, + portRangeEnd: editForm.portRangeEnd, + allowedDomains: editForm.allowedDomains, + allowedIps: editForm.allowedIps, + }); + if (res.code === 0) { + toast.success("编辑成功"); + setEditShareOpen(false); + loadShares(); + } else { + toast.error(res.msg || "编辑失败"); + } + } catch { + toast.error("网络错误"); + } + }; + const handleImportNode = async () => { if (!importForm.remoteUrl || !importForm.token) { toast.error("请填写完整信息"); @@ -326,6 +385,13 @@ export default function PanelSharingPage() {

{share.name}

+ + + + + + {/* Import Node Modal */} setImportNodeOpen(false)}> From 14063e66c3d55319b46b66c951370ede856d6b9f Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:59:02 +0800 Subject: [PATCH 3/6] Update README with project modifications and disclaimers Removed outdated project details and added modifications section. --- README.md | 63 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 66ae164..414e43d 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,7 @@ > **联系我们**: [Telegram群组](https://t.me/flvxpanel) -## Original Project -- **Name**: flux-panel -- **Source**: https://github.com/bqlpfy/flux-panel -- **License**: Apache License 2.0 -## Modifications -The following major changes and additions have been made in this fork (FLVX): - -### 1. Backend Architecture (Replaced) -- **Removed**: The original `springboot-backend/` (Java/Spring Boot) has been entirely removed. -- **Added**: A new `go-backend/` (Go/SQLite) implementation replaces the original backend. - -### 2. Forwarding Agent (Modified) -- **Modified**: `go-gost/` - Modified forwarding agent wrapper. -- **Modified**: `go-gost/x/` - Modified local fork of the `gost` extensions library. - -### 3. Frontend (Modified) -- **Modified**: `vite-frontend/` - Significant updates to the React/Vite dashboard to compatible with the new Go backend, including UI/UX improvements (HeroUI + Tailwind). - -### 4. Mobile Applications (Removed) -- **Removed**: `android-app/` - Source code for the Android client. -- **Removed**: `ios-app/` - Source code for the iOS client. - -### 5. Infrastructure & Scripts -- **Modified**: `docker-compose-v4.yml`, `docker-compose-v6.yml` (Updated for Go backend). -- **Modified**: `install.sh`, `panel_install.sh` (Updated installation logic). -- **Added**: `AGENTS.md` (Project documentation). - ---- ## 特性 - 支持按 **隧道账号级别** 管理流量转发数量,可用于用户/隧道配额控制 @@ -39,6 +11,10 @@ The following major changes and additions have been made in this fork (FLVX): - 可针对 **指定用户的指定隧道进行限速** 设置 - 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型 - 提供灵活的转发策略配置,适用于多种网络场景 +- 面板分享,支持将节点分享给其他人,面板对接面板 +- 支持分组权限管理,隧道分组、用户分组 +- 支持批量功能,可以批量下发配置,启停等 +- 支持隧道修改配置、转发修改隧道 ## 部署流程 @@ -73,6 +49,37 @@ curl -L https://github.com/Sagit-chu/flux-panel/releases/download/2.1.0/install. > ⚠️ 首次登录后请立即修改默认密码! +--- +## Original Project +- **Name**: flux-panel +- **Source**: https://github.com/bqlpfy/flux-panel +- **License**: Apache License 2.0 + +## Modifications +The following major changes and additions have been made in this fork (FLVX): + +### 1. Backend Architecture (Replaced) +- **Removed**: The original `springboot-backend/` (Java/Spring Boot) has been entirely removed. +- **Added**: A new `go-backend/` (Go/SQLite) implementation replaces the original backend. + +### 2. Forwarding Agent (Modified) +- **Modified**: `go-gost/` - Modified forwarding agent wrapper. +- **Modified**: `go-gost/x/` - Modified local fork of the `gost` extensions library. + +### 3. Frontend (Modified) +- **Modified**: `vite-frontend/` - Significant updates to the React/Vite dashboard to compatible with the new Go backend, including UI/UX improvements (HeroUI + Tailwind). + +### 4. Mobile Applications (Removed) +- **Removed**: `android-app/` - Source code for the Android client. +- **Removed**: `ios-app/` - Source code for the iOS client. + +### 5. Infrastructure & Scripts +- **Modified**: `docker-compose-v4.yml`, `docker-compose-v6.yml` (Updated for Go backend). +- **Modified**: `install.sh`, `panel_install.sh` (Updated installation logic). +- **Added**: `AGENTS.md` (Project documentation). + +--- + ## 免责声明 From 87605ce8f8aca2bbdccd5604ba36baa7fa1c1ad9 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 03:07:42 +0000 Subject: [PATCH 4/6] fix(federation): sync remote node status on list and detect deleted provider shares Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- go-backend/internal/http/client/federation.go | 8 +++ .../internal/http/handler/federation.go | 69 +++++++++++++++++++ go-backend/internal/http/handler/handler.go | 3 + 3 files changed, 80 insertions(+) diff --git a/go-backend/internal/http/client/federation.go b/go-backend/internal/http/client/federation.go index fdc04ee..128055a 100644 --- a/go-backend/internal/http/client/federation.go +++ b/go-backend/internal/http/client/federation.go @@ -85,6 +85,14 @@ func NewFederationClient() *FederationClient { } } +func NewFederationClientWithTimeout(timeout time.Duration) *FederationClient { + return &FederationClient{ + client: &http.Client{ + Timeout: timeout, + }, + } +} + func (c *FederationClient) Connect(url, token, localDomain string) (*RemoteNodeInfo, error) { url = strings.TrimSuffix(url, "/") req, err := http.NewRequest("POST", url+"/api/v1/federation/connect", nil) diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index 4f6c26f..625357f 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -8,6 +8,7 @@ import ( "net/http" "sort" "strings" + "sync" "time" "go-backend/internal/http/client" @@ -1402,6 +1403,74 @@ func isPeerIPAllowed(clientIP net.IP, whitelist string) bool { return false } +func (h *Handler) syncRemoteNodeStatuses(items []map[string]interface{}) { + type remoteEntry struct { + index int + remoteURL string + remoteToken string + } + + var remotes []remoteEntry + for i, item := range items { + isRemote, _ := item["isRemote"].(int) + if isRemote != 1 { + continue + } + url, _ := item["remoteUrl"].(string) + token, _ := item["remoteToken"].(string) + url = strings.TrimSpace(url) + token = strings.TrimSpace(token) + if url == "" || token == "" { + continue + } + remotes = append(remotes, remoteEntry{index: i, remoteURL: url, remoteToken: token}) + } + if len(remotes) == 0 { + return + } + + localDomain := h.federationLocalDomain() + fc := client.NewFederationClientWithTimeout(5 * time.Second) + + type syncResult struct { + index int + status int + syncError string + } + + results := make([]syncResult, len(remotes)) + var wg sync.WaitGroup + for i, entry := range remotes { + wg.Add(1) + go func(idx int, e remoteEntry) { + defer wg.Done() + info, err := fc.Connect(e.remoteURL, e.remoteToken, localDomain) + if err != nil { + errMsg := err.Error() + if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "Invalid token") || strings.Contains(errMsg, "Unauthorized") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_deleted"} + } else if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "Share is disabled") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_disabled"} + } else if strings.Contains(errMsg, "Share expired") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_expired"} + } else { + results[idx] = syncResult{index: e.index, status: 0, syncError: errMsg} + } + } else { + results[idx] = syncResult{index: e.index, status: info.Status, syncError: ""} + } + }(i, entry) + } + wg.Wait() + + for _, r := range results { + items[r.index]["status"] = r.status + if r.syncError != "" { + items[r.index]["syncError"] = r.syncError + } + } +} + func (h *Handler) cleanupPeerShareRuntimes(shareID int64) { if h == nil || h.repo == nil || shareID <= 0 { return diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index a0ce67c..9319956 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -321,6 +321,9 @@ func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.Err(-2, err.Error())) return } + + h.syncRemoteNodeStatuses(items) + response.WriteJSON(w, response.OK(items)) } From 896f2bc5f8a3a9a46faff03d32fd05203ce34351 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 03:07:59 +0000 Subject: [PATCH 5/6] fix(panel-sharing): display provider share deletion warnings for remote nodes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- vite-frontend/src/pages/node.tsx | 15 ++++++++++++++- vite-frontend/src/pages/panel-sharing.tsx | 12 ++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index 178c0b5..225e8cf 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -65,6 +65,7 @@ interface Node { status: number; isRemote?: number; remoteUrl?: string; + syncError?: string; connectionStatus: "online" | "offline"; systemInfo?: { cpuUsage: number; @@ -219,7 +220,8 @@ export default function NodePage() { const nodesData: Node[] = (res.data || []).map((node: any) => ({ ...node, inx: node.inx ?? 0, - connectionStatus: node.status === 1 ? "online" : "offline", + connectionStatus: node.syncError ? "offline" : node.status === 1 ? "online" : "offline", + syncError: node.syncError || undefined, systemInfo: null, copyLoading: false, })); @@ -1181,6 +1183,17 @@ export default function NodePage() { + {isRemoteNode && node.syncError && ( +
+ {node.syncError === "provider_share_deleted" + ? "提供方已删除该分享" + : node.syncError === "provider_share_disabled" + ? "提供方已禁用该分享" + : node.syncError === "provider_share_expired" + ? "提供方分享已过期" + : `远程同步失败: ${node.syncError}`} +
+ )} {/* 基础信息 */}
diff --git a/vite-frontend/src/pages/panel-sharing.tsx b/vite-frontend/src/pages/panel-sharing.tsx index 2b5205a..df05e73 100644 --- a/vite-frontend/src/pages/panel-sharing.tsx +++ b/vite-frontend/src/pages/panel-sharing.tsx @@ -78,6 +78,7 @@ interface RemoteUsageNode { usedPorts: number[]; bindings: RemoteUsageBinding[]; activeBindingNum: number; + syncError?: string; } export default function PanelSharingPage() { @@ -456,6 +457,17 @@ export default function PanelSharingPage() { 绑定 {node.activeBindingNum || 0} + {node.syncError && ( +
+ {node.syncError === "provider_share_deleted" + ? "提供方已删除该分享" + : node.syncError === "provider_share_disabled" + ? "提供方已禁用该分享" + : node.syncError === "provider_share_expired" + ? "提供方分享已过期" + : `远程同步失败: ${node.syncError}`} +
+ )} {node.remoteUrl &&

远程地址: {node.remoteUrl}

}

共享ID: {node.shareId || "-"}

端口范围: {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}

From 65a61054694f0caa277481e6c0356dd6a9414c74 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 05:59:11 +0000 Subject: [PATCH 6/6] feat(node-install): add gcode.hostcentral.cc proxy to install script download URL --- go-backend/internal/http/handler/mutations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 9784d04..41893c2 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -413,7 +413,7 @@ func (h *Handler) nodeInstall(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.Err(-2, err.Error())) return } - cmd := fmt.Sprintf("curl -L https://github.com/Sagit-chu/flux-panel/releases/latest/download/install.sh -o ./install.sh && chmod +x ./install.sh && ./install.sh -a %s -s %s", processServerAddress(panelAddr), secret) + cmd := fmt.Sprintf("curl -L https://gcode.hostcentral.cc/https://github.com/Sagit-chu/flux-panel/releases/latest/download/install.sh -o ./install.sh && chmod +x ./install.sh && ./install.sh -a %s -s %s", processServerAddress(panelAddr), secret) response.WriteJSON(w, response.OK(cmd)) }