mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-13 00:14:22 +08:00
Compare commits
2 Commits
3.0.1-rc14
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8339f52280 | |||
| 2e85234612 |
@@ -103,10 +103,10 @@ jobs:
|
|||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Run PostgreSQL contract test
|
- name: Run PostgreSQL contract tests
|
||||||
env:
|
env:
|
||||||
FLVX_POSTGRES_TEST_DSN: 'postgres://flux_test:flux_test_pass@127.0.0.1:5432/flux_test?sslmode=disable'
|
FLVX_POSTGRES_TEST_DSN: 'postgres://flux_test:flux_test_pass@127.0.0.1:5432/flux_test?sslmode=disable'
|
||||||
run: go test ./tests/contract -run TestPostgresNodeCreateRepairsMissingIDDefaultContract -count=1
|
run: go test ./tests/contract -run 'TestPostgres(NodeCreateRepairsMissingIDDefaultContract|NewRelayTunnelEmitsMatchingPrivateEndpointAuthentication)$' -count=1
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
name: Build Agent
|
name: Build Agent
|
||||||
|
|||||||
@@ -25,6 +25,27 @@ Go 后端和 agent 的最低编译版本为 **Go 1.26.10**;若使用 Go 1.27
|
|||||||
|
|
||||||
## 部署流程
|
## 部署流程
|
||||||
---
|
---
|
||||||
|
### 让 AI Agent 帮你安装(复制即用)
|
||||||
|
|
||||||
|
将下面整段提示词复制给能执行终端命令的 AI Agent(如 Codex、Claude Code)。Agent 已运行在目标 Linux 服务器上时可直接安装;远程安装时,提供 SSH 主机别名或连接方式,使用已有 SSH 密钥登录即可,无需把私钥贴进对话。
|
||||||
|
|
||||||
|
```text
|
||||||
|
请帮我在目标 Linux 服务器上安装 FLVX 流量转发面板,直接执行安装并完成验证。
|
||||||
|
|
||||||
|
项目仓库:https://github.com/Sagit-chu/flvx
|
||||||
|
安装文档:https://github.com/Sagit-chu/flvx/blob/main/doc/install.md
|
||||||
|
|
||||||
|
1. 确认目标服务器:如果当前终端就在目标 Linux 服务器上,直接使用当前环境;否则使用我提供的 SSH 连接。缺少连接信息时,只询问必要信息,确认目标后再执行安装。
|
||||||
|
2. 先阅读项目 README、安装文档及将要执行的安装脚本,检查系统、架构、root/sudo 权限、Docker、Compose、端口占用和已有 FLVX 容器、数据卷及部署文件。已有部署时先确认是更新还是新装,保留原配置和数据。
|
||||||
|
3. 新安装默认使用仓库标记为 latest 的 Release、Docker Compose、SQLite,部署目录 /opt/flvx-panel,前端端口 6366、后端端口 6365。我已指定的版本、目录或端口优先;端口冲突时先与我确认替代端口。
|
||||||
|
4. 缺少依赖时按该发行版的官方方式安装 Docker Engine 和 Compose 插件。进入部署目录,从项目 Releases 下载所选版本的 panel_install.sh,先检查下载成功和脚本内容,再执行。使用交互式终端操作菜单,选择“安装面板”并按上述配置输入;以脚本实际提示为准。
|
||||||
|
5. 默认直连 GitHub,可设置 PROXY_ENABLED=false;连接失败时说明原因,再与我确认是否使用代理。脚本可能修改 Docker IPv6 配置并重启 Docker;若会影响现有服务,先说明影响并确认。保留现有防火墙和 SSH 规则,仅按需放行实际使用的面板端口;云安全组无法操作时列出需要我放行的端口。
|
||||||
|
6. 安装后检查 Compose 服务状态、后端健康检查、前端 HTTP 页面及后端 /flow/test 的响应,并从实际访问端验证连通性。失败时查看相关日志并修复后重试;受网络或权限限制无法完成的检查要明确说明,不能直接宣称安装成功。
|
||||||
|
7. 最后给我实际安装版本、部署目录、访问地址、验证结果,以及在该目录查看状态、查看日志和重启服务的命令。说明默认管理员账号和密码均为 admin_user,提醒首次登录立即修改密码;不要输出 .env 中的 JWT_SECRET、数据库密码等密钥。
|
||||||
|
```
|
||||||
|
|
||||||
|
需要安装转发节点时,告诉 Agent 按[节点端部署文档](doc/install.md#二节点端部署)操作,并提供节点服务器、面板后端地址及从面板获取的节点接入密钥;安装完成后以服务正常运行且面板显示节点在线为准。
|
||||||
|
|
||||||
### Docker Compose部署
|
### Docker Compose部署
|
||||||
#### 快速部署(安装最新版)
|
#### 快速部署(安装最新版)
|
||||||
面板端:
|
面板端:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"go-backend/internal/relaysecurity"
|
"go-backend/internal/relaysecurity"
|
||||||
"go-backend/internal/store/model"
|
"go-backend/internal/store/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -22,7 +24,8 @@ func (r *Repository) RelayEndpointCredentials(tx *gorm.DB, tunnelID, nodeID int6
|
|||||||
err := find()
|
err := find()
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) && create {
|
if errors.Is(err, gorm.ErrRecordNotFound) && create {
|
||||||
var existing int64
|
var existing int64
|
||||||
if e := db.Model(&model.ChainTunnel{}).Where("tunnel_id = ? AND node_id = ? AND chain_type = ?", tunnelID, nodeID, role).Count(&existing).Error; e != nil {
|
// chain_tunnel stores roles as text; relay_endpoint_auth uses integers.
|
||||||
|
if e := db.Model(&model.ChainTunnel{}).Where("tunnel_id = ? AND node_id = ? AND chain_type = ?", tunnelID, nodeID, strconv.Itoa(role)).Count(&existing).Error; e != nil {
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
if existing > 0 {
|
if existing > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-backend/internal/relaysecurity"
|
||||||
|
"go-backend/internal/store/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRelayEndpointCredentialsBindsLegacyChainTypeAsText(t *testing.T) {
|
||||||
|
for _, role := range []int{2, 3} {
|
||||||
|
t.Run(strconv.Itoa(role), func(t *testing.T) {
|
||||||
|
r, err := Open(filepath.Join(t.TempDir(), "relay.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
|
||||||
|
// SQLite coerces integer parameters for this legacy text column.
|
||||||
|
// Check the actual repository query with pgx's text encoder as well,
|
||||||
|
// so the PostgreSQL regression is caught without a running server.
|
||||||
|
checked := 0
|
||||||
|
if err := r.DB().Callback().Query().After("gorm:query").Register("test:relay_chain_text_parameter", func(db *gorm.DB) {
|
||||||
|
if db.Statement.Table != "chain_tunnel" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checked++
|
||||||
|
if len(db.Statement.Vars) != 3 {
|
||||||
|
db.AddError(fmt.Errorf("expected three chain lookup arguments, got %d", len(db.Statement.Vars)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encoded, err := pgtype.NewMap().Encode(pgtype.TextOID, pgtype.TextFormatCode, db.Statement.Vars[2], nil)
|
||||||
|
if err != nil {
|
||||||
|
db.AddError(fmt.Errorf("encode chain_type for PostgreSQL: %w", err))
|
||||||
|
} else if string(encoded) != strconv.Itoa(role) {
|
||||||
|
db.AddError(fmt.Errorf("unexpected chain_type argument: %q", encoded))
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := model.Tunnel{Name: "relay", Type: 2, Flow: 1, RelayAuthMode: relaysecurity.Required}
|
||||||
|
if err := r.DB().Create(&tunnel).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const nodeID = int64(7)
|
||||||
|
if err := r.DB().Transaction(func(tx *gorm.DB) error {
|
||||||
|
created, err := r.RelayEndpointCredentials(tx, tunnel.ID, nodeID, role, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, create := range []bool{false, true} {
|
||||||
|
loaded, err := r.RelayEndpointCredentials(tx, tunnel.ID, nodeID, role, create)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *created != *loaded {
|
||||||
|
return fmt.Errorf("reloading endpoint changed credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create and reload endpoint: %v", err)
|
||||||
|
}
|
||||||
|
if checked != 1 {
|
||||||
|
t.Fatalf("expected one chain lookup for a new endpoint, got %d", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
chain := model.ChainTunnel{TunnelID: tunnel.ID, NodeID: nodeID, ChainType: strconv.Itoa(role)}
|
||||||
|
if err := r.DB().Create(&chain).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.DB().Where("tunnel_id = ?", tunnel.ID).Delete(&model.RelayEndpointAuth{}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := r.RelayEndpointCredentials(nil, tunnel.ID, nodeID, role, true); err == nil || !strings.Contains(err.Error(), "refusing regeneration") {
|
||||||
|
t.Fatalf("missing protected credentials must fail closed, got %v", err)
|
||||||
|
}
|
||||||
|
if checked != 2 {
|
||||||
|
t.Fatalf("expected chain lookup before refusing regeneration, got %d", checked)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := r.DB().Model(&model.RelayEndpointAuth{}).Count(&count).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatal("regenerated credentials for an existing protected endpoint")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -440,13 +440,15 @@ func TestFederationDualPanelRemoteEntryRuntimeContract(t *testing.T) {
|
|||||||
func insertContractNode(t *testing.T, r *repo.Repository, name, ip, portRange, secret string, status int) int64 {
|
func insertContractNode(t *testing.T, r *repo.Repository, name, ip, portRange, secret string, status int) int64 {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
if err := r.DB().Exec(`
|
var id int64
|
||||||
|
if err := r.DB().Raw(`
|
||||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, name, secret, ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0).Error; err != nil {
|
RETURNING id
|
||||||
|
`, name, secret, ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0).Row().Scan(&id); err != nil {
|
||||||
t.Fatalf("insert node %s: %v", name, err)
|
t.Fatalf("insert node %s: %v", name, err)
|
||||||
}
|
}
|
||||||
return mustLastInsertID(t, r, name)
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertPeerShare(t *testing.T, r *repo.Repository, share *repo.PeerShare) int64 {
|
func insertPeerShare(t *testing.T, r *repo.Repository, share *repo.PeerShare) int64 {
|
||||||
|
|||||||
@@ -11,11 +11,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-backend/internal/auth"
|
"go-backend/internal/auth"
|
||||||
|
httpserver "go-backend/internal/http"
|
||||||
|
"go-backend/internal/http/handler"
|
||||||
"go-backend/internal/store/repo"
|
"go-backend/internal/store/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
func TestNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
||||||
router, r := setupContractRouter(t, "relay-auth-contract")
|
router, r := setupContractRouter(t, "relay-auth-contract")
|
||||||
|
assertNewRelayTunnelEndpointAuthentication(t, router, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgresNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
||||||
|
r := ownerNamesRepository(t, "postgres")()
|
||||||
|
router := httpserver.NewRouter(handler.New(r, "relay-auth-contract"), "relay-auth-contract")
|
||||||
|
assertNewRelayTunnelEndpointAuthentication(t, router, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNewRelayTunnelEndpointAuthentication(t *testing.T, router http.Handler, r *repo.Repository) {
|
||||||
|
t.Helper()
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
token, err := auth.GenerateToken(1, "admin", 0, "relay-auth-contract")
|
token, err := auth.GenerateToken(1, "admin", 0, "relay-auth-contract")
|
||||||
|
|||||||
Reference in New Issue
Block a user