diff --git a/openflare_relay/Dockerfile b/openflare_relay/Dockerfile
index 67b0402b..5ba2580c 100644
--- a/openflare_relay/Dockerfile
+++ b/openflare_relay/Dockerfile
@@ -9,24 +9,15 @@ COPY openflare_relay /openflare_relay
WORKDIR /openflare_relay
RUN CGO_ENABLED=0 GOOS=linux go build -o openflare-relay ./cmd/relay
-# Extract frps binary from official image
-FROM fatedier/frps:v0.69.0 AS frps
-
# Final runtime image
-FROM alpine:latest
-
-RUN apk --no-cache add ca-certificates tzdata
+FROM fatedier/frps:v0.69.0
# Copy openflare-relay binary
COPY --from=builder /openflare_relay/openflare-relay /usr/local/bin/openflare-relay
-# Copy frps binary from official image
-COPY --from=frps /usr/bin/frps /usr/local/bin/frps
-
VOLUME ["/var/lib/openflare-relay"]
-ENV OPENFLARE_FRPS_PATH=/usr/local/bin/frps
+ENV OPENFLARE_FRPS_PATH=/usr/bin/frps
ENV OPENFLARE_DATA_DIR=/var/lib/openflare-relay
ENTRYPOINT ["/usr/local/bin/openflare-relay"]
-CMD ["-config", "/etc/openflare-relay/relay.json"]
diff --git a/openflare_server/controller/agent.go b/openflare_server/controller/agent.go
index 2f8a3b2e..5ceaa341 100644
--- a/openflare_server/controller/agent.go
+++ b/openflare_server/controller/agent.go
@@ -89,6 +89,23 @@ func AgentHeartbeat(c *gin.Context) {
// @Success 200 {object} map[string]interface{}
// @Router /api/agent/config-versions/active [get]
func AgentGetActiveConfig(c *gin.Context) {
+ authNode, ok := c.Get("node")
+ if !ok {
+ respondUnauthorized(c, "Node object missing from context")
+ return
+ }
+ node := authNode.(*model.Node)
+
+ if node.NodeType == "tunnel_client" {
+ config, err := service.GetFlaredTunnelConfig(node)
+ if err != nil {
+ respondFailure(c, "无法生成隧道配置: "+err.Error())
+ return
+ }
+ respondSuccess(c, config)
+ return
+ }
+
config, err := service.GetActiveConfigForAgent()
if err != nil {
respondFailure(c, "当前没有激活版本")
diff --git a/openflare_server/controller/flared.go b/openflare_server/controller/flared.go
deleted file mode 100644
index b0d08a5a..00000000
--- a/openflare_server/controller/flared.go
+++ /dev/null
@@ -1,171 +0,0 @@
-package controller
-
-import (
- "log/slog"
- "net"
- "openflare/model"
- "openflare/service"
- "time"
-
- "github.com/gin-gonic/gin"
- "golang.org/x/net/websocket"
-)
-
-// FlaredHeartbeat godoc
-// @Summary Report OpenFlared client heartbeat
-// @Tags Flared
-// @Accept json
-// @Produce json
-// @Security TunnelTokenAuth
-// @Param payload body service.FlaredHeartbeatPayload true "Flared heartbeat payload"
-// @Success 200 {object} map[string]interface{}
-// @Failure 400 {object} map[string]interface{}
-// @Router /api/flared/heartbeat [post]
-func FlaredHeartbeat(c *gin.Context) {
- var payload service.FlaredHeartbeatPayload
- if !bindJSON(c, &payload) {
- return
- }
- authTunnel, ok := c.Get("tunnel")
- if !ok {
- respondUnauthorized(c, "无权进行此操作")
- return
- }
- tunnel := authTunnel.(*model.Tunnel)
- result, err := service.HeartbeatFlared(tunnel, payload)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, result)
-}
-
-// FlaredGetActiveConfig godoc
-// @Summary Get active tunnel config for OpenFlared client
-// @Tags Flared
-// @Produce json
-// @Security TunnelTokenAuth
-// @Success 200 {object} map[string]interface{}
-// @Router /api/flared/config/active [get]
-func FlaredGetActiveConfig(c *gin.Context) {
- authTunnel, ok := c.Get("tunnel")
- if !ok {
- respondUnauthorized(c, "无权进行此操作")
- return
- }
- tunnel := authTunnel.(*model.Tunnel)
- config, err := service.GetFlaredTunnelConfig(tunnel)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, config)
-}
-
-// FlaredReportApplyLog godoc
-// @Summary Report apply log for OpenFlared client
-// @Tags Flared
-// @Accept json
-// @Produce json
-// @Security TunnelTokenAuth
-// @Param payload body service.ApplyLogPayload true "Apply log payload"
-// @Success 200 {object} map[string]interface{}
-// @Failure 400 {object} map[string]interface{}
-// @Router /api/flared/apply-log [post]
-func FlaredReportApplyLog(c *gin.Context) {
- var payload service.ApplyLogPayload
- if !bindJSON(c, &payload) {
- return
- }
- authTunnel, ok := c.Get("tunnel")
- if !ok {
- respondUnauthorized(c, "无权进行此操作")
- return
- }
- tunnel := authTunnel.(*model.Tunnel)
- payload.NodeID = tunnel.TunnelID
- log, err := service.ReportApplyLog(payload)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, log)
-}
-
-// FlaredWebSocket godoc
-// @Summary Upgrade OpenFlared connection to websocket
-// @Tags Flared
-// @Security TunnelTokenAuth
-// @Router /api/flared/ws [get]
-func FlaredWebSocket(c *gin.Context) {
- authTunnel, ok := c.Get("tunnel")
- if !ok {
- respondUnauthorized(c, "无权进行此操作")
- return
- }
- tunnel := authTunnel.(*model.Tunnel)
- slog.Debug("flared ws upgrade requested", "tunnel_id", tunnel.TunnelID, "remote", c.Request.RemoteAddr)
- websocket.Handler(func(conn *websocket.Conn) {
- client := service.RegisterFlaredWSClient(tunnel.TunnelID)
- defer service.UnregisterFlaredWSClient(client)
- defer func() {
- _ = conn.Close()
- slog.Debug("flared ws connection closed", "tunnel_id", tunnel.TunnelID)
- }()
-
- slog.Debug("flared ws upgrade succeeded", "tunnel_id", tunnel.TunnelID, "remote", c.Request.RemoteAddr)
-
- go func() {
- <-client.Done()
- _ = conn.Close()
- }()
-
- go streamFlaredWSMessages(c, conn, client)
-
- for {
- var message service.WSMessage
- _ = conn.SetReadDeadline(time.Now().Add(agentWSReadTimeout()))
- if err := websocket.JSON.Receive(conn, &message); err != nil {
- if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
- slog.Debug("flared ws receive timeout", "tunnel_id", tunnel.TunnelID)
- return
- }
- slog.Debug("flared ws receive failed", "tunnel_id", tunnel.TunnelID, "error", err)
- return
- }
- slog.Debug("flared ws message received", "tunnel_id", tunnel.TunnelID, "type", message.Type)
- switch message.Type {
- case "status":
- // Handle status if needed for flared
- case "ping":
- if !service.SendFlaredWSPong(tunnel.TunnelID) {
- slog.Debug("flared ws pong enqueue failed", "tunnel_id", tunnel.TunnelID)
- }
- case "pong":
- slog.Debug("flared ws pong received", "tunnel_id", tunnel.TunnelID)
- default:
- slog.Debug("flared ws unsupported message type", "tunnel_id", tunnel.TunnelID, "type", message.Type)
- }
- }
- }).ServeHTTP(c.Writer, c.Request)
-}
-
-func streamFlaredWSMessages(c *gin.Context, conn *websocket.Conn, client *service.WSClient) {
- for {
- select {
- case <-c.Request.Context().Done():
- return
- case <-client.Done():
- return
- case message, ok := <-client.Messages():
- if !ok {
- return
- }
- _ = conn.SetWriteDeadline(time.Now().Add(agentWSWriteTimeout()))
- if err := websocket.JSON.Send(conn, message); err != nil {
- slog.Debug("flared ws send failed", "tunnel_id", client.ID(), "error", err)
- return
- }
- }
- }
-}
diff --git a/openflare_server/controller/tunnel.go b/openflare_server/controller/tunnel.go
deleted file mode 100644
index 6a8bb2ec..00000000
--- a/openflare_server/controller/tunnel.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package controller
-
-import (
- "openflare/service"
-
- "github.com/gin-gonic/gin"
-)
-
-func GetTunnels(c *gin.Context) {
- tunnels, err := service.ListTunnels()
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, tunnels)
-}
-
-func GetTunnel(c *gin.Context) {
- id, ok := parseIDParam(c)
- if !ok {
- return
- }
- tunnel, err := service.GetTunnel(id)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, tunnel)
-}
-
-func CreateTunnel(c *gin.Context) {
- var input service.TunnelInput
- if !bindJSON(c, &input) {
- return
- }
- tunnel, err := service.CreateTunnel(input)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, tunnel)
-}
-
-func UpdateTunnel(c *gin.Context) {
- id, ok := parseIDParam(c)
- if !ok {
- return
- }
- var input service.TunnelInput
- if !bindJSON(c, &input) {
- return
- }
- tunnel, err := service.UpdateTunnel(id, input)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, tunnel)
-}
-
-func DeleteTunnel(c *gin.Context) {
- id, ok := parseIDParam(c)
- if !ok {
- return
- }
- if err := service.DeleteTunnel(id); err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, nil)
-}
-
-func RotateTunnelToken(c *gin.Context) {
- id, ok := parseIDParam(c)
- if !ok {
- return
- }
- tunnel, err := service.RotateTunnelToken(id)
- if err != nil {
- respondFailure(c, err.Error())
- return
- }
- respondSuccess(c, tunnel)
-}
diff --git a/openflare_server/middleware/tunnel-auth.go b/openflare_server/middleware/tunnel-auth.go
deleted file mode 100644
index 9f8d69bc..00000000
--- a/openflare_server/middleware/tunnel-auth.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package middleware
-
-import (
- "github.com/gin-gonic/gin"
- "net/http"
- "openflare/service"
-)
-
-// TunnelAuth authenticates OpenFlared client requests using X-Tunnel-Token.
-func TunnelAuth() func(c *gin.Context) {
- return func(c *gin.Context) {
- token := c.GetHeader("X-Tunnel-Token")
- tunnel, err := service.AuthenticateTunnelToken(token)
- if err != nil {
- c.JSON(http.StatusUnauthorized, gin.H{
- "success": false,
- "message": "无权进行此操作,Tunnel Token 无效",
- })
- c.Abort()
- return
- }
- c.Set("tunnel", tunnel)
- c.Next()
- }
-}
diff --git a/openflare_server/model/main.go b/openflare_server/model/main.go
index 233de054..e5e1f4cb 100644
--- a/openflare_server/model/main.go
+++ b/openflare_server/model/main.go
@@ -34,7 +34,7 @@ func registeredModels() []any {
&ProxyRoute{},
&ConfigVersion{},
&Node{},
- &Tunnel{},
+
&NodeSystemProfile{},
&ApplyLog{},
&NodeMetricSnapshot{},
diff --git a/openflare_server/model/migrate/v19.go b/openflare_server/model/migrate/v19.go
new file mode 100644
index 00000000..ea6d28fb
--- /dev/null
+++ b/openflare_server/model/migrate/v19.go
@@ -0,0 +1,82 @@
+package migrate
+
+import (
+ "fmt"
+ "log/slog"
+
+ "gorm.io/gorm"
+)
+
+type proxyRouteV19 struct {
+ ID uint `gorm:"primaryKey"`
+ TunnelID *uint `gorm:"column:tunnel_id"`
+ TunnelNodeID *uint `gorm:"column:tunnel_node_id"`
+ UpstreamType string `gorm:"column:upstream_type"`
+}
+
+func (proxyRouteV19) TableName() string {
+ return "proxy_routes"
+}
+
+func init() {
+ Register(V19())
+}
+
+func V19() Migration {
+ return Migration{
+ FromVersion: 18,
+ ToVersion: 19,
+ Migrate: migrateV19,
+ Validate: validateV19,
+ }
+}
+
+func migrateV19(ctx Context, db *gorm.DB, backend string) error {
+ // Drop tunnels table
+ if db.Migrator().HasTable("tunnels") {
+ if err := db.Migrator().DropTable("tunnels"); err != nil {
+ return fmt.Errorf("failed to drop tunnels table: %w", err)
+ }
+ slog.Info("dropped tunnels table")
+ }
+
+ // Add tunnel_node_id column
+ if !db.Migrator().HasColumn(&proxyRouteV19{}, "tunnel_node_id") {
+ if err := db.Migrator().AddColumn(&proxyRouteV19{}, "TunnelNodeID"); err != nil {
+ return fmt.Errorf("failed to add tunnel_node_id to proxy_routes: %w", err)
+ }
+ slog.Info("added tunnel_node_id column to proxy_routes")
+ }
+
+ // Drop old tunnel_id column
+ if db.Migrator().HasColumn(&proxyRouteV19{}, "tunnel_id") {
+ // Update routes that previously used tunnel to be disabled or direct to prevent dangling refs
+ db.Model(&proxyRouteV19{}).Where("upstream_type = ?", "tunnel").Update("upstream_type", "direct")
+ if err := db.Migrator().DropColumn(&proxyRouteV19{}, "tunnel_id"); err != nil {
+ return fmt.Errorf("failed to drop tunnel_id column from proxy_routes: %w", err)
+ }
+ slog.Info("dropped tunnel_id column from proxy_routes")
+ }
+
+ return ctx.ApplyCurrentSchema(db, backend)
+}
+
+func validateV19(ctx Context, db *gorm.DB, backend string) error {
+ if err := ctx.ValidateDatabaseSchemaVersion(db, backend, 18); err != nil {
+ return err
+ }
+
+ if db.Migrator().HasTable("tunnels") {
+ return fmt.Errorf("table tunnels should be dropped in v19")
+ }
+
+ if !db.Migrator().HasColumn(&proxyRouteV19{}, "tunnel_node_id") {
+ return fmt.Errorf("column proxy_routes.tunnel_node_id is missing")
+ }
+
+ if db.Migrator().HasColumn(&proxyRouteV19{}, "tunnel_id") {
+ return fmt.Errorf("column proxy_routes.tunnel_id should be dropped in v19")
+ }
+
+ return nil
+}
diff --git a/openflare_server/model/migrations.go b/openflare_server/model/migrations.go
index 70b2f7cd..0be1b9a2 100644
--- a/openflare_server/model/migrations.go
+++ b/openflare_server/model/migrations.go
@@ -1164,7 +1164,7 @@ func validateDatabaseSchemaV16(db *gorm.DB, backend string) error {
if err := validateDatabaseSchemaV15(db, backend); err != nil {
return err
}
- if !db.Migrator().HasTable(&Tunnel{}) {
+ if !db.Migrator().HasTable("tunnels") {
return fmt.Errorf("table tunnels is missing")
}
if !db.Migrator().HasColumn(&Node{}, "node_type") {
diff --git a/openflare_server/model/proxy_route.go b/openflare_server/model/proxy_route.go
index 26cadc86..158fb34b 100644
--- a/openflare_server/model/proxy_route.go
+++ b/openflare_server/model/proxy_route.go
@@ -31,7 +31,7 @@ type ProxyRoute struct {
BasicAuthPassword string `json:"basic_auth_password" gorm:"size:255;not null;default:''"`
Remark string `json:"remark" gorm:"size:255"`
UpstreamType string `json:"upstream_type" gorm:"size:32;not null;default:'direct'"`
- TunnelID *uint `json:"tunnel_id" gorm:"index"`
+ TunnelNodeID *uint `json:"tunnel_node_id" gorm:"index"`
TunnelTargetAddr string `json:"tunnel_target_addr" gorm:"size:512"`
TunnelTargetProtocol string `json:"tunnel_target_protocol" gorm:"size:16"`
CreatedAt time.Time `json:"created_at"`
@@ -92,7 +92,7 @@ func (route *ProxyRoute) Update() error {
"basic_auth_password": route.BasicAuthPassword,
"remark": route.Remark,
"upstream_type": route.UpstreamType,
- "tunnel_id": route.TunnelID,
+ "tunnel_node_id": route.TunnelNodeID,
"tunnel_target_addr": route.TunnelTargetAddr,
"tunnel_target_protocol": route.TunnelTargetProtocol,
}).Error
diff --git a/openflare_server/model/tunnel.go b/openflare_server/model/tunnel.go
deleted file mode 100644
index 362f3340..00000000
--- a/openflare_server/model/tunnel.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package model
-
-import "time"
-
-type Tunnel struct {
- ID uint `json:"id" gorm:"primaryKey"`
- TunnelID string `json:"tunnel_id" gorm:"uniqueIndex;size:64;not null"`
- Name string `json:"name" gorm:"size:128;not null"`
- TunnelToken string `json:"-" gorm:"size:128;index"`
- Status string `json:"status" gorm:"size:16;not null;default:'offline'"`
- ClientVersion string `json:"client_version" gorm:"size:64"`
- FrpVersion string `json:"frp_version" gorm:"size:64"`
- LastSeenAt time.Time `json:"last_seen_at"`
- LastError string `json:"last_error" gorm:"type:text"`
- CurrentVersion string `json:"current_version" gorm:"size:32"`
- CurrentChecksum string `json:"current_checksum" gorm:"size:64"`
- ConnectedRelays string `json:"connected_relays" gorm:"type:text;not null;default:'[]'"`
- Remark string `json:"remark" gorm:"size:255"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-func ListTunnels() (tunnels []*Tunnel, err error) {
- err = DB.Order("id desc").Find(&tunnels).Error
- return tunnels, err
-}
-
-func GetTunnelByID(id uint) (*Tunnel, error) {
- tunnel := &Tunnel{}
- err := DB.First(tunnel, id).Error
- return tunnel, err
-}
-
-func GetTunnelByTunnelID(tunnelID string) (*Tunnel, error) {
- tunnel := &Tunnel{}
- err := DB.Where("tunnel_id = ?", tunnelID).First(tunnel).Error
- return tunnel, err
-}
-
-func GetTunnelByTunnelToken(token string) (*Tunnel, error) {
- tunnel := &Tunnel{}
- err := DB.Where("tunnel_token = ?", token).First(tunnel).Error
- return tunnel, err
-}
-
-func (tunnel *Tunnel) Insert() error {
- return DB.Create(tunnel).Error
-}
-
-func (tunnel *Tunnel) Update() error {
- return DB.Save(tunnel).Error
-}
-
-func (tunnel *Tunnel) Delete() error {
- return DB.Delete(tunnel).Error
-}
diff --git a/openflare_server/router/api-router.go b/openflare_server/router/api-router.go
index ea371c89..8d81709c 100644
--- a/openflare_server/router/api-router.go
+++ b/openflare_server/router/api-router.go
@@ -198,16 +198,7 @@ func SetApiRouter(router *gin.Engine) {
applyLogRoute.GET("/", controller.GetApplyLogs)
applyLogRoute.POST("/cleanup", controller.CleanupApplyLogs)
}
- tunnelRoute := apiRouter.Group("/tunnels")
- tunnelRoute.Use(middleware.AdminAuth())
- {
- tunnelRoute.GET("/", controller.GetTunnels)
- tunnelRoute.GET("/:id", controller.GetTunnel)
- tunnelRoute.POST("/", controller.CreateTunnel)
- tunnelRoute.POST("/:id/update", controller.UpdateTunnel)
- tunnelRoute.POST("/:id/delete", controller.DeleteTunnel)
- tunnelRoute.POST("/:id/rotate-token", controller.RotateTunnelToken)
- }
+
accessLogRoute := apiRouter.Group("/access-logs")
accessLogRoute.Use(middleware.AdminAuth())
{
@@ -240,13 +231,6 @@ func SetApiRouter(router *gin.Engine) {
relayRoute.POST("/heartbeat", controller.RelayHeartbeat)
relayRoute.GET("/ws", controller.RelayWebSocket)
}
- flaredRoute := apiRouter.Group("/flared")
- flaredRoute.Use(middleware.TunnelAuth())
- {
- flaredRoute.POST("/heartbeat", controller.FlaredHeartbeat)
- flaredRoute.GET("/config/active", controller.FlaredGetActiveConfig)
- flaredRoute.POST("/apply-log", controller.FlaredReportApplyLog)
- flaredRoute.GET("/ws", controller.FlaredWebSocket)
- }
+
}
}
diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go
index 94b2e784..638441df 100644
--- a/openflare_server/service/config_version.go
+++ b/openflare_server/service/config_version.go
@@ -356,7 +356,6 @@ func PublishConfigVersion(createdBy string, force bool) (*ReleaseResult, error)
Checksum: record.Checksum,
}
BroadcastAgentWSActiveConfig(activeConfig)
- BroadcastFlaredWSActiveConfig(activeConfig)
return &ReleaseResult{
Version: record,
Routes: bundle.Routes,
diff --git a/openflare_server/service/flared_ws.go b/openflare_server/service/flared_ws.go
deleted file mode 100644
index 54e5decf..00000000
--- a/openflare_server/service/flared_ws.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package service
-
-import (
- "log/slog"
-)
-
-var DefaultFlaredWSHub = NewWSHub("flared")
-
-func RegisterFlaredWSClient(tunnelID string) *WSClient {
- return DefaultFlaredWSHub.Register(tunnelID)
-}
-
-func UnregisterFlaredWSClient(client *WSClient) {
- DefaultFlaredWSHub.Unregister(client)
-}
-
-func IsFlaredWSConnected(tunnelID string) bool {
- return DefaultFlaredWSHub.IsConnected(tunnelID)
-}
-
-func SendFlaredWSPong(tunnelID string) bool {
- return DefaultFlaredWSHub.SendMessage(tunnelID, WSMessage{
- Type: "pong",
- })
-}
-
-func SendFlaredWSActiveConfig(tunnelID string, activeConfig *ActiveConfigMeta) bool {
- if activeConfig == nil {
- return false
- }
- return DefaultFlaredWSHub.SendMessage(tunnelID, WSMessage{
- Type: "active_config",
- Payload: activeConfig,
- })
-}
-
-func BroadcastFlaredWSActiveConfig(activeConfig *ActiveConfigMeta) WSBroadcastResult {
- if activeConfig == nil {
- slog.Debug("flared ws broadcast skipped because active config is nil")
- return WSBroadcastResult{}
- }
- res := DefaultFlaredWSHub.Broadcast(WSMessage{
- Type: "active_config",
- Payload: activeConfig,
- })
- slog.Debug("flared ws broadcast active config",
- "version", activeConfig.Version,
- "checksum", activeConfig.Checksum,
- "client_count", res.ClientCount,
- "success_count", res.SuccessCount,
- "failed_tunnels", res.FailedIDs,
- )
- return res
-}
diff --git a/openflare_server/service/node.go b/openflare_server/service/node.go
index 2045b358..cd2b8765 100644
--- a/openflare_server/service/node.go
+++ b/openflare_server/service/node.go
@@ -634,6 +634,8 @@ func normalizeNodeType(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "tunnel_relay":
return "tunnel_relay"
+ case "tunnel_client":
+ return "tunnel_client"
default:
return "edge_node"
}
diff --git a/openflare_server/service/proxy_route.go b/openflare_server/service/proxy_route.go
index 8f7abf7f..04dc342f 100644
--- a/openflare_server/service/proxy_route.go
+++ b/openflare_server/service/proxy_route.go
@@ -62,7 +62,7 @@ type ProxyRouteInput struct {
BasicAuthPassword string `json:"basic_auth_password"`
Remark string `json:"remark"`
UpstreamType string `json:"upstream_type"`
- TunnelID *uint `json:"tunnel_id"`
+ TunnelNodeID *uint `json:"tunnel_node_id"`
TunnelTargetAddr string `json:"tunnel_target_addr"`
TunnelTargetProtocol string `json:"tunnel_target_protocol"`
}
@@ -101,7 +101,7 @@ type ProxyRouteView struct {
BasicAuthPassword string `json:"basic_auth_password"`
Remark string `json:"remark"`
UpstreamType string `json:"upstream_type"`
- TunnelID *uint `json:"tunnel_id"`
+ TunnelNodeID *uint `json:"tunnel_node_id"`
TunnelTargetAddr string `json:"tunnel_target_addr"`
TunnelTargetProtocol string `json:"tunnel_target_protocol"`
CreatedAt time.Time `json:"created_at"`
@@ -327,11 +327,11 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro
route.Remark = remark
route.UpstreamType = upstreamType
if upstreamType == "tunnel" {
- route.TunnelID = input.TunnelID
+ route.TunnelNodeID = input.TunnelNodeID
route.TunnelTargetAddr = strings.TrimSpace(input.TunnelTargetAddr)
route.TunnelTargetProtocol = normalizeTunnelTargetProtocol(input.TunnelTargetProtocol)
} else {
- route.TunnelID = nil
+ route.TunnelNodeID = nil
route.TunnelTargetAddr = ""
route.TunnelTargetProtocol = ""
}
@@ -421,7 +421,7 @@ func buildProxyRouteView(route *model.ProxyRoute) (*ProxyRouteView, error) {
BasicAuthPassword: route.BasicAuthPassword,
Remark: route.Remark,
UpstreamType: route.UpstreamType,
- TunnelID: route.TunnelID,
+ TunnelNodeID: route.TunnelNodeID,
TunnelTargetAddr: route.TunnelTargetAddr,
TunnelTargetProtocol: route.TunnelTargetProtocol,
CreatedAt: route.CreatedAt,
diff --git a/openflare_server/service/relay.go b/openflare_server/service/relay.go
index f1d77987..579e2296 100644
--- a/openflare_server/service/relay.go
+++ b/openflare_server/service/relay.go
@@ -1,7 +1,6 @@
package service
import (
- "encoding/json"
"fmt"
"log/slog"
"openflare/common"
@@ -162,42 +161,10 @@ type FlaredProxyEntry struct {
CustomDomains []string `json:"custom_domains"`
}
-// HeartbeatFlared processes an OpenFlared client heartbeat.
-func HeartbeatFlared(tunnel *model.Tunnel, payload FlaredHeartbeatPayload) (*FlaredHeartbeatResponse, error) {
- if tunnel == nil {
- return nil, fmt.Errorf("tunnel is nil")
- }
- slog.Debug("flared heartbeat received", "tunnel_id", tunnel.TunnelID)
-
- now := time.Now()
- tunnel.ClientVersion = strings.TrimSpace(payload.ClientVersion)
- tunnel.FrpVersion = strings.TrimSpace(payload.FrpVersion)
- tunnel.Status = "online"
- tunnel.LastSeenAt = now
- tunnel.CurrentVersion = strings.TrimSpace(payload.CurrentVersion)
- tunnel.CurrentChecksum = strings.TrimSpace(payload.CurrentChecksum)
-
- relaysJSON, err := json.Marshal(payload.ConnectedRelays)
- if err == nil {
- tunnel.ConnectedRelays = string(relaysJSON)
- }
-
- if err := tunnel.Update(); err != nil {
- return nil, fmt.Errorf("update tunnel heartbeat: %w", err)
- }
-
- activeConfig, _ := GetActiveConfigMetaForAgent()
-
- return &FlaredHeartbeatResponse{
- ActiveConfig: activeConfig,
- TunnelSettings: buildRelaySettings(),
- }, nil
-}
-
// GetFlaredTunnelConfig builds the full tunnel routing config for an OpenFlared client.
-func GetFlaredTunnelConfig(tunnel *model.Tunnel) (*FlaredTunnelConfigResponse, error) {
- if tunnel == nil {
- return nil, fmt.Errorf("tunnel is nil")
+func GetFlaredTunnelConfig(node *model.Node) (*FlaredTunnelConfigResponse, error) {
+ if node == nil {
+ return nil, fmt.Errorf("node is nil")
}
activeVersion, err := model.GetActiveConfigVersion()
@@ -237,7 +204,7 @@ func GetFlaredTunnelConfig(tunnel *model.Tunnel) (*FlaredTunnelConfigResponse, e
// Build proxy entries from routes
proxies := make([]FlaredProxyEntry, 0)
for _, route := range routes {
- if route.UpstreamType != "tunnel" || route.TunnelID == nil || *route.TunnelID != tunnel.ID {
+ if route.UpstreamType != "tunnel" || route.TunnelNodeID == nil || *route.TunnelNodeID != node.ID {
continue
}
if !route.Enabled {
@@ -249,7 +216,7 @@ func GetFlaredTunnelConfig(tunnel *model.Tunnel) (*FlaredTunnelConfigResponse, e
}
localAddr, localPort := parseTunnelTargetAddr(route.TunnelTargetAddr)
proxies = append(proxies, FlaredProxyEntry{
- Name: fmt.Sprintf("%s-%s", tunnel.TunnelID, sanitizeProxyName(domains[0])),
+ Name: fmt.Sprintf("%s-%s", node.NodeID, sanitizeProxyName(domains[0])),
Type: "http",
LocalAddr: localAddr,
LocalPort: localPort,
diff --git a/openflare_server/service/tunnel.go b/openflare_server/service/tunnel.go
deleted file mode 100644
index 06389168..00000000
--- a/openflare_server/service/tunnel.go
+++ /dev/null
@@ -1,183 +0,0 @@
-package service
-
-import (
- "encoding/json"
- "errors"
- "log/slog"
- "openflare/model"
- "strings"
- "time"
-)
-
-type TunnelInput struct {
- Name string `json:"name"`
- Remark string `json:"remark"`
-}
-
-type TunnelView struct {
- ID uint `json:"id"`
- TunnelID string `json:"tunnel_id"`
- Name string `json:"name"`
- TunnelToken string `json:"tunnel_token"`
- Status string `json:"status"`
- ClientVersion string `json:"client_version"`
- FrpVersion string `json:"frp_version"`
- LastSeenAt time.Time `json:"last_seen_at"`
- LastError string `json:"last_error"`
- CurrentVersion string `json:"current_version"`
- CurrentChecksum string `json:"current_checksum"`
- ConnectedRelays []string `json:"connected_relays"`
- Remark string `json:"remark"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt time.Time `json:"updated_at"`
-}
-
-func ListTunnels() ([]*TunnelView, error) {
- tunnels, err := model.ListTunnels()
- if err != nil {
- return nil, err
- }
- views := make([]*TunnelView, 0, len(tunnels))
- for _, tunnel := range tunnels {
- views = append(views, buildTunnelView(tunnel))
- }
- return views, nil
-}
-
-func GetTunnel(id uint) (*TunnelView, error) {
- tunnel, err := model.GetTunnelByID(id)
- if err != nil {
- return nil, err
- }
- return buildTunnelView(tunnel), nil
-}
-
-func CreateTunnel(input TunnelInput) (*TunnelView, error) {
- name := strings.TrimSpace(input.Name)
- if name == "" {
- return nil, errors.New("Tunnel 名称不能为空")
- }
- tunnelID, err := newTunnelID()
- if err != nil {
- return nil, err
- }
- tunnelToken, err := newRandomToken()
- if err != nil {
- return nil, err
- }
- tunnel := &model.Tunnel{
- TunnelID: tunnelID,
- Name: name,
- TunnelToken: tunnelToken,
- Status: "offline",
- Remark: strings.TrimSpace(input.Remark),
- }
- if err := tunnel.Insert(); err != nil {
- if model.IsUniqueConstraintError(err) {
- return nil, errors.New("Tunnel 标识生成冲突,请重试")
- }
- return nil, err
- }
- slog.Info("tunnel created", "name", tunnel.Name, "tunnel_id", tunnel.TunnelID)
- return buildTunnelView(tunnel), nil
-}
-
-func UpdateTunnel(id uint, input TunnelInput) (*TunnelView, error) {
- tunnel, err := model.GetTunnelByID(id)
- if err != nil {
- return nil, err
- }
- name := strings.TrimSpace(input.Name)
- if name == "" {
- return nil, errors.New("Tunnel 名称不能为空")
- }
- tunnel.Name = name
- tunnel.Remark = strings.TrimSpace(input.Remark)
- if err := tunnel.Update(); err != nil {
- return nil, err
- }
- slog.Info("tunnel updated", "name", tunnel.Name, "tunnel_id", tunnel.TunnelID)
- return buildTunnelView(tunnel), nil
-}
-
-func DeleteTunnel(id uint) error {
- tunnel, err := model.GetTunnelByID(id)
- if err != nil {
- return err
- }
- slog.Info("tunnel deleted", "name", tunnel.Name, "tunnel_id", tunnel.TunnelID)
- return tunnel.Delete()
-}
-
-func RotateTunnelToken(id uint) (*TunnelView, error) {
- tunnel, err := model.GetTunnelByID(id)
- if err != nil {
- return nil, err
- }
- newToken, err := newRandomToken()
- if err != nil {
- return nil, err
- }
- tunnel.TunnelToken = newToken
- if err := tunnel.Update(); err != nil {
- return nil, err
- }
- slog.Info("tunnel token rotated", "tunnel_id", tunnel.TunnelID)
- return buildTunnelView(tunnel), nil
-}
-
-func AuthenticateTunnelToken(token string) (*model.Tunnel, error) {
- token = strings.TrimSpace(token)
- if token == "" {
- return nil, errors.New("缺少 Tunnel Token")
- }
- tunnel, err := model.GetTunnelByTunnelToken(token)
- if err != nil {
- return nil, errors.New("Tunnel Token 无效")
- }
- return tunnel, nil
-}
-
-func buildTunnelView(tunnel *model.Tunnel) *TunnelView {
- if tunnel == nil {
- return nil
- }
- relays := decodeTunnelConnectedRelays(tunnel.ConnectedRelays)
- return &TunnelView{
- ID: tunnel.ID,
- TunnelID: tunnel.TunnelID,
- Name: tunnel.Name,
- TunnelToken: tunnel.TunnelToken,
- Status: tunnel.Status,
- ClientVersion: tunnel.ClientVersion,
- FrpVersion: tunnel.FrpVersion,
- LastSeenAt: tunnel.LastSeenAt,
- LastError: tunnel.LastError,
- CurrentVersion: tunnel.CurrentVersion,
- CurrentChecksum: tunnel.CurrentChecksum,
- ConnectedRelays: relays,
- Remark: tunnel.Remark,
- CreatedAt: tunnel.CreatedAt,
- UpdatedAt: tunnel.UpdatedAt,
- }
-}
-
-func decodeTunnelConnectedRelays(raw string) []string {
- raw = strings.TrimSpace(raw)
- if raw == "" || raw == "[]" {
- return make([]string, 0)
- }
- var relays []string
- if err := json.Unmarshal([]byte(raw), &relays); err != nil {
- return make([]string, 0)
- }
- return relays
-}
-
-func newTunnelID() (string, error) {
- token, err := newRandomToken()
- if err != nil {
- return "", err
- }
- return "tun-" + token, nil
-}
diff --git a/openflare_server/web/app/(dashboard)/tunnel/page.tsx b/openflare_server/web/app/(dashboard)/tunnel/page.tsx
deleted file mode 100644
index 963b24b3..00000000
--- a/openflare_server/web/app/(dashboard)/tunnel/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { Metadata } from 'next';
-
-import { TunnelsPage } from '@/features/tunnels/components/tunnels-page';
-
-export const metadata: Metadata = {
- title: '内网穿透 - OpenFlare',
-};
-
-export default function Page() {
- return
| 代理名称 | +类型 | +状态 | +客户端地址 | +最近连接 | +
|---|---|---|---|---|
| + {proxy.name} + | +{proxy.type} | +
+ |
+ {proxy.client_addr} | ++ {isMeaningfulTime(proxy.last_start_time) + ? formatRelativeTime(proxy.last_start_time) + : '—'} + | +
- 方式一:Docker Run -
-- 方式二:Docker Compose (docker-compose.yml) -
-- 隧道 Token (仅供高级用法) -
-- {tunnel.tunnel_token} -
-| 名称 / 隧道ID | -状态 | -版本 / 校验 | -已连接 Relay | -操作 | -
|---|---|---|---|---|
|
-
-
- - {tunnel.name} - -- {tunnel.tunnel_id.slice(0, 8)}... - - {tunnel.remark ? ( -- {tunnel.remark} - - ) : null} - |
-
-
-
- - 活跃:{formatRelativeTime(tunnel.last_seen_at)} - - ) : null} - |
-
-
-
- - 版本:{tunnel.current_version || '未知'} - -- {tunnel.current_checksum - ? `签名:${tunnel.current_checksum.slice(0, 12)}...` - : '签名:未知'} - - |
-
- {tunnel.connected_relays && tunnel.connected_relays.length > 0 ? (
-
- {tunnel.connected_relays.map((relay, index) => (
-
- ) : (
- - {relay} - - ))} -暂无 - )} - |
-
-
-
-
-
-
-
-
-
- |
-