mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-08 00:26:37 +08:00
[优化] 增加 FRPS WebUI 支持,添加相关配置和数据库迁移
This commit is contained in:
@@ -45,7 +45,7 @@ func main() {
|
|||||||
stateStore := state.NewStore(cfg.StatePath)
|
stateStore := state.NewStore(cfg.StatePath)
|
||||||
_ = stateStore // In the future we may use stateStore for auth caching
|
_ = stateStore // In the future we may use stateStore for auth caching
|
||||||
|
|
||||||
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir)
|
frpsManager := frps.NewManager(cfg.FrpsPath, cfg.DataDir, cfg.InitialAuthToken())
|
||||||
|
|
||||||
slog.Info("detected frps version", "version", frpsManager.GetVersion())
|
slog.Info("detected frps version", "version", frpsManager.GetVersion())
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package frps
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -19,6 +21,7 @@ type Manager struct {
|
|||||||
frpsPath string
|
frpsPath string
|
||||||
dataDir string
|
dataDir string
|
||||||
configPath string
|
configPath string
|
||||||
|
agentToken string
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
activeConfig *service.RelayConfig
|
activeConfig *service.RelayConfig
|
||||||
@@ -34,15 +37,18 @@ type RuntimeStatus struct {
|
|||||||
LastError string
|
LastError string
|
||||||
Connections int
|
Connections int
|
||||||
ProxyCount int
|
ProxyCount int
|
||||||
|
ClientCount int
|
||||||
|
Proxies []service.RelayProxyStat
|
||||||
ProcessAlive bool
|
ProcessAlive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(frpsPath string, dataDir string) *Manager {
|
func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
frpsPath: frpsPath,
|
frpsPath: frpsPath,
|
||||||
dataDir: dataDir,
|
dataDir: dataDir,
|
||||||
configPath: filepath.Join(dataDir, "frps.toml"),
|
configPath: filepath.Join(dataDir, "frps.toml"),
|
||||||
status: "unhealthy",
|
status: "unhealthy",
|
||||||
|
agentToken: agentToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +71,139 @@ func (m *Manager) GetStatus() string {
|
|||||||
return m.status
|
return m.status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type frpsServerInfo struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
BindPort int `json:"bind_port"`
|
||||||
|
CurConns int `json:"cur_conns"`
|
||||||
|
ClientCounts int `json:"client_counts"`
|
||||||
|
ProxyTypeCount map[string]int `json:"proxy_type_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type frpsProxyInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ClientAddr string `json:"client_addr"`
|
||||||
|
LastStartTime string `json:"last_start_time"`
|
||||||
|
LastCloseTime string `json:"last_close_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type frpsProxiesResponse struct {
|
||||||
|
Proxies []frpsProxyInfo `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) fetchFrpsTelemetry() (connections int, proxyCount int, clientCount int, proxies []service.RelayProxyStat, err error) {
|
||||||
|
m.mu.RLock()
|
||||||
|
activeConfig := m.activeConfig
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
if activeConfig == nil {
|
||||||
|
return 0, 0, 0, nil, fmt.Errorf("activeConfig is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboardPort := activeConfig.BindPort + 500
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Fetch serverinfo
|
||||||
|
infoURL := fmt.Sprintf("http://127.0.0.1:%d/api/serverinfo", dashboardPort)
|
||||||
|
req, err := http.NewRequest(http.MethodGet, infoURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, nil, err
|
||||||
|
}
|
||||||
|
password := m.agentToken
|
||||||
|
if password == "" {
|
||||||
|
password = "admin"
|
||||||
|
}
|
||||||
|
req.SetBasicAuth("admin", password)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, 0, nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return 0, 0, 0, nil, fmt.Errorf("serverinfo returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var info frpsServerInfo
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||||
|
return 0, 0, 0, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
connections = info.CurConns
|
||||||
|
clientCount = info.ClientCounts
|
||||||
|
|
||||||
|
totalProxies := 0
|
||||||
|
for _, count := range info.ProxyTypeCount {
|
||||||
|
totalProxies += count
|
||||||
|
}
|
||||||
|
proxyCount = totalProxies
|
||||||
|
|
||||||
|
// 2. Fetch HTTP proxies list
|
||||||
|
proxiesURL := fmt.Sprintf("http://127.0.0.1:%d/api/proxy/http", dashboardPort)
|
||||||
|
req2, err := http.NewRequest(http.MethodGet, proxiesURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return connections, proxyCount, clientCount, nil, nil
|
||||||
|
}
|
||||||
|
req2.SetBasicAuth("admin", password)
|
||||||
|
resp2, err := client.Do(req2)
|
||||||
|
if err != nil {
|
||||||
|
return connections, proxyCount, clientCount, nil, nil
|
||||||
|
}
|
||||||
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
if resp2.StatusCode == http.StatusOK {
|
||||||
|
var listResp frpsProxiesResponse
|
||||||
|
if err := json.NewDecoder(resp2.Body).Decode(&listResp); err == nil {
|
||||||
|
for _, p := range listResp.Proxies {
|
||||||
|
proxies = append(proxies, service.RelayProxyStat{
|
||||||
|
Name: p.Name,
|
||||||
|
Type: p.Type,
|
||||||
|
Status: p.Status,
|
||||||
|
ClientAddr: p.ClientAddr,
|
||||||
|
LastStartTime: p.LastStartTime,
|
||||||
|
LastCloseTime: p.LastCloseTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections, proxyCount, clientCount, proxies, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) GetRuntimeStatus() RuntimeStatus {
|
func (m *Manager) GetRuntimeStatus() RuntimeStatus {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
status := m.status
|
||||||
|
lastError := m.lastError
|
||||||
|
cmd := m.cmd
|
||||||
|
m.mu.RUnlock()
|
||||||
|
|
||||||
|
connections := 0
|
||||||
|
proxyCount := 0
|
||||||
|
clientCount := 0
|
||||||
|
var proxies []service.RelayProxyStat
|
||||||
|
|
||||||
|
if cmd != nil && cmd.Process != nil && status == "healthy" {
|
||||||
|
if conns, pCount, cCount, pxs, err := m.fetchFrpsTelemetry(); err == nil {
|
||||||
|
connections = conns
|
||||||
|
proxyCount = pCount
|
||||||
|
clientCount = cCount
|
||||||
|
proxies = pxs
|
||||||
|
} else {
|
||||||
|
slog.Debug("failed to fetch frps telemetry", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return RuntimeStatus{
|
return RuntimeStatus{
|
||||||
Status: m.status,
|
Status: status,
|
||||||
LastError: m.lastError,
|
LastError: lastError,
|
||||||
Connections: 0,
|
Connections: connections,
|
||||||
ProxyCount: 0,
|
ProxyCount: proxyCount,
|
||||||
ProcessAlive: m.cmd != nil && m.cmd.Process != nil,
|
ClientCount: clientCount,
|
||||||
|
Proxies: proxies,
|
||||||
|
ProcessAlive: cmd != nil && cmd.Process != nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +218,8 @@ func (m *Manager) UpdateConfig(cfg *service.RelayConfig) {
|
|||||||
if m.activeConfig != nil &&
|
if m.activeConfig != nil &&
|
||||||
m.activeConfig.BindPort == cfg.BindPort &&
|
m.activeConfig.BindPort == cfg.BindPort &&
|
||||||
m.activeConfig.VhostHTTPPort == cfg.VhostHTTPPort &&
|
m.activeConfig.VhostHTTPPort == cfg.VhostHTTPPort &&
|
||||||
m.activeConfig.AuthToken == cfg.AuthToken {
|
m.activeConfig.AuthToken == cfg.AuthToken &&
|
||||||
|
m.activeConfig.WebServerEnabled == cfg.WebServerEnabled {
|
||||||
if m.cmd == nil && !m.stopping {
|
if m.cmd == nil && !m.stopping {
|
||||||
slog.Warn("frps config unchanged but process is not running, restarting")
|
slog.Warn("frps config unchanged but process is not running, restarting")
|
||||||
if err := m.restartProcess(); err != nil {
|
if err := m.restartProcess(); err != nil {
|
||||||
@@ -136,6 +267,22 @@ func (m *Manager) renderConfig(cfg *service.RelayConfig) error {
|
|||||||
buf.WriteString(fmt.Sprintf("token = \"%s\"\n", cfg.AuthToken))
|
buf.WriteString(fmt.Sprintf("token = \"%s\"\n", cfg.AuthToken))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebServer configuration
|
||||||
|
buf.WriteString("\n[webServer]\n")
|
||||||
|
if cfg.WebServerEnabled {
|
||||||
|
buf.WriteString("addr = \"0.0.0.0\"\n")
|
||||||
|
} else {
|
||||||
|
buf.WriteString("addr = \"127.0.0.1\"\n")
|
||||||
|
}
|
||||||
|
buf.WriteString(fmt.Sprintf("port = %d\n", cfg.BindPort+500))
|
||||||
|
buf.WriteString("user = \"admin\"\n")
|
||||||
|
|
||||||
|
password := m.agentToken
|
||||||
|
if password == "" {
|
||||||
|
password = "admin"
|
||||||
|
}
|
||||||
|
buf.WriteString(fmt.Sprintf("password = \"%s\"\n", password))
|
||||||
|
|
||||||
return os.WriteFile(m.configPath, buf.Bytes(), 0644)
|
return os.WriteFile(m.configPath, buf.Bytes(), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,16 +54,18 @@ func (s *Service) doHeartbeat(ctx context.Context) {
|
|||||||
|
|
||||||
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
runtimeStatus := s.frpsManager.GetRuntimeStatus()
|
||||||
payload := service.RelayHeartbeatPayload{
|
payload := service.RelayHeartbeatPayload{
|
||||||
Version: config.Version,
|
Version: config.Version,
|
||||||
ExtVersion: s.frpsManager.GetVersion(),
|
ExtVersion: s.frpsManager.GetVersion(),
|
||||||
RelayStatus: runtimeStatus.Status,
|
RelayStatus: runtimeStatus.Status,
|
||||||
FrpsConnCount: runtimeStatus.Connections,
|
FrpsConnCount: runtimeStatus.Connections,
|
||||||
FrpsProxyCount: runtimeStatus.ProxyCount,
|
FrpsProxyCount: runtimeStatus.ProxyCount,
|
||||||
Name: s.config.NodeName,
|
FrpsClientCount: runtimeStatus.ClientCount,
|
||||||
IP: s.config.NodeIP,
|
FrpsProxies: runtimeStatus.Proxies,
|
||||||
Profile: observability.BuildProfile(s.config, s.stateStore),
|
Name: s.config.NodeName,
|
||||||
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
IP: s.config.NodeIP,
|
||||||
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
Profile: observability.BuildProfile(s.config, s.stateStore),
|
||||||
|
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
|
||||||
|
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := s.client.Heartbeat(ctx, payload)
|
resp, err := s.client.Heartbeat(ctx, payload)
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package migrate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nodeV17 struct{}
|
||||||
|
|
||||||
|
func (nodeV17) TableName() string {
|
||||||
|
return "nodes"
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(V17())
|
||||||
|
}
|
||||||
|
|
||||||
|
func V17() Migration {
|
||||||
|
return Migration{
|
||||||
|
FromVersion: 16,
|
||||||
|
ToVersion: 17,
|
||||||
|
Migrate: migrateV17,
|
||||||
|
Validate: validateV17,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateV17(ctx Context, db *gorm.DB, backend string) error {
|
||||||
|
if err := ctx.ApplyCurrentSchema(db, backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateV17(ctx Context, db *gorm.DB, backend string) error {
|
||||||
|
if err := ctx.ValidateDatabaseSchemaVersion(db, backend, 16); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db == nil {
|
||||||
|
return fmt.Errorf("database handle is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := db.Migrator()
|
||||||
|
if !migrator.HasColumn(&nodeV17{}, "relay_web_server_enabled") {
|
||||||
|
return fmt.Errorf("column nodes.relay_web_server_enabled is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate columns on a sharded partition table
|
||||||
|
for _, shard := range []string{"node_observation_frps_00"} {
|
||||||
|
for _, column := range []string{"frps_client_count", "frps_proxies"} {
|
||||||
|
if !migrator.HasColumn(shard, column) {
|
||||||
|
return fmt.Errorf("column %s.%s is missing", shard, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -86,6 +86,8 @@ func (databaseSchemaMigrationContext) ValidateDatabaseSchemaVersion(db *gorm.DB,
|
|||||||
return validateDatabaseSchemaV15(db, backend)
|
return validateDatabaseSchemaV15(db, backend)
|
||||||
case 16:
|
case 16:
|
||||||
return validateDatabaseSchemaV16(db, backend)
|
return validateDatabaseSchemaV16(db, backend)
|
||||||
|
case 17:
|
||||||
|
return validateDatabaseSchemaV17(db, backend)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("database schema validation for v%d is not defined", version)
|
return fmt.Errorf("database schema validation for v%d is not defined", version)
|
||||||
}
|
}
|
||||||
@@ -1301,6 +1303,31 @@ func validateDatabaseSchemaV16(db *gorm.DB, backend string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateDatabaseSchemaV17(db *gorm.DB, backend string) error {
|
||||||
|
if err := validateDatabaseSchemaV16(db, backend); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db == nil {
|
||||||
|
return fmt.Errorf("database handle is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator := db.Migrator()
|
||||||
|
if !migrator.HasColumn(&Node{}, "relay_web_server_enabled") {
|
||||||
|
return fmt.Errorf("column nodes.relay_web_server_enabled is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate columns on a sharded partition table
|
||||||
|
for _, shard := range []string{"node_observation_frps_00"} {
|
||||||
|
for _, column := range []string{"frps_client_count", "frps_proxies"} {
|
||||||
|
if !migrator.HasColumn(shard, column) {
|
||||||
|
return fmt.Errorf("column %s.%s is missing", shard, column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func databaseSchemaMigrations() []databaseSchemaMigration {
|
func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||||
ctx := databaseSchemaMigrationContext{}
|
ctx := databaseSchemaMigrationContext{}
|
||||||
migrations := []databaseSchemaMigration{}
|
migrations := []databaseSchemaMigration{}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type Node struct {
|
|||||||
RelayClientAccessAddr string `json:"relay_client_access_addr" gorm:"size:255"`
|
RelayClientAccessAddr string `json:"relay_client_access_addr" gorm:"size:255"`
|
||||||
RelayClientProxyURL string `json:"relay_client_proxy_url" gorm:"size:512"`
|
RelayClientProxyURL string `json:"relay_client_proxy_url" gorm:"size:512"`
|
||||||
RelayStatus string `json:"relay_status" gorm:"size:16;not null;default:'unknown'"`
|
RelayStatus string `json:"relay_status" gorm:"size:16;not null;default:'unknown'"`
|
||||||
|
RelayWebServerEnabled bool `json:"relay_web_server_enabled" gorm:"not null;default:false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListNodes() (nodes []*Node, err error) {
|
func ListNodes() (nodes []*Node, err error) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ type NodeObservationFrps struct {
|
|||||||
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
CapturedAt time.Time `json:"captured_at" gorm:"index"`
|
||||||
FrpsConnections int `json:"frps_connections"`
|
FrpsConnections int `json:"frps_connections"`
|
||||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||||
|
FrpsClientCount int `json:"frps_client_count"`
|
||||||
|
FrpsProxies string `json:"frps_proxies" gorm:"type:text"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ type NodeView struct {
|
|||||||
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
||||||
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
||||||
RelayStatus string `json:"relay_status"`
|
RelayStatus string `json:"relay_status"`
|
||||||
|
RelayWebServerEnabled bool `json:"relay_web_server_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatResponse, error) {
|
func HeartbeatNode(node *model.Node, payload AgentNodePayload) (*HeartbeatResponse, error) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type NodeInput struct {
|
|||||||
RelayAgentAccessAddr string `json:"relay_agent_access_addr"`
|
RelayAgentAccessAddr string `json:"relay_agent_access_addr"`
|
||||||
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
RelayClientAccessAddr string `json:"relay_client_access_addr"`
|
||||||
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
RelayClientProxyURL string `json:"relay_client_proxy_url"`
|
||||||
|
RelayWebServerEnabled bool `json:"relay_web_server_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeAgentUpdateInput struct {
|
type NodeAgentUpdateInput struct {
|
||||||
@@ -100,6 +101,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
|
|||||||
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
||||||
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
||||||
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
||||||
|
node.RelayWebServerEnabled = input.RelayWebServerEnabled
|
||||||
}
|
}
|
||||||
if !node.GeoManualOverride {
|
if !node.GeoManualOverride {
|
||||||
applyGeoInfoFromIP(node, node.IP)
|
applyGeoInfoFromIP(node, node.IP)
|
||||||
@@ -137,6 +139,7 @@ func UpdateNode(id uint, input NodeInput) (*NodeView, error) {
|
|||||||
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
node.RelayAgentAccessAddr = strings.TrimSpace(input.RelayAgentAccessAddr)
|
||||||
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
node.RelayClientAccessAddr = strings.TrimSpace(input.RelayClientAccessAddr)
|
||||||
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
node.RelayClientProxyURL = strings.TrimSpace(input.RelayClientProxyURL)
|
||||||
|
node.RelayWebServerEnabled = input.RelayWebServerEnabled
|
||||||
if input.RelayBindPort > 0 {
|
if input.RelayBindPort > 0 {
|
||||||
node.RelayBindPort = input.RelayBindPort
|
node.RelayBindPort = input.RelayBindPort
|
||||||
}
|
}
|
||||||
@@ -354,6 +357,7 @@ func buildNodeView(node *model.Node) *NodeView {
|
|||||||
view.RelayClientAccessAddr = node.RelayClientAccessAddr
|
view.RelayClientAccessAddr = node.RelayClientAccessAddr
|
||||||
view.RelayClientProxyURL = node.RelayClientProxyURL
|
view.RelayClientProxyURL = node.RelayClientProxyURL
|
||||||
view.RelayStatus = node.RelayStatus
|
view.RelayStatus = node.RelayStatus
|
||||||
|
view.RelayWebServerEnabled = node.RelayWebServerEnabled
|
||||||
view.Version = node.Version
|
view.Version = node.Version
|
||||||
view.ExtVersion = node.ExtVersion
|
view.ExtVersion = node.ExtVersion
|
||||||
return view
|
return view
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"openflare/model"
|
"openflare/model"
|
||||||
"time"
|
"time"
|
||||||
@@ -146,24 +147,44 @@ func buildRelayDashboardSnapshot(node *model.Node, obs *model.NodeObservationFrp
|
|||||||
}
|
}
|
||||||
totalProxies := 0
|
totalProxies := 0
|
||||||
totalConnections := 0
|
totalConnections := 0
|
||||||
|
clientCounts := 0
|
||||||
|
proxies := []RelayProxyStat{}
|
||||||
|
|
||||||
if obs != nil {
|
if obs != nil {
|
||||||
totalProxies = obs.FrpsProxyCount
|
totalProxies = obs.FrpsProxyCount
|
||||||
totalConnections = obs.FrpsConnections
|
totalConnections = obs.FrpsConnections
|
||||||
|
clientCounts = obs.FrpsClientCount
|
||||||
|
if obs.FrpsProxies != "" {
|
||||||
|
var decoded []RelayProxyStat
|
||||||
|
if err := json.Unmarshal([]byte(obs.FrpsProxies), &decoded); err == nil {
|
||||||
|
proxies = decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if totalProxies < 0 {
|
if totalProxies < 0 {
|
||||||
totalProxies = 0
|
totalProxies = 0
|
||||||
}
|
}
|
||||||
onlineProxies := totalProxies
|
onlineProxies := 0
|
||||||
if node.RelayStatus != "healthy" {
|
for _, p := range proxies {
|
||||||
onlineProxies = 0
|
if p.Status == "online" {
|
||||||
|
onlineProxies++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Fallback for backward compatibility
|
||||||
|
if len(proxies) == 0 {
|
||||||
|
onlineProxies = totalProxies
|
||||||
|
if node.RelayStatus != "healthy" {
|
||||||
|
onlineProxies = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &RelayDashboardSnapshot{
|
return &RelayDashboardSnapshot{
|
||||||
TotalProxies: totalProxies,
|
TotalProxies: totalProxies,
|
||||||
OnlineProxies: onlineProxies,
|
OnlineProxies: onlineProxies,
|
||||||
OfflineProxies: totalProxies - onlineProxies,
|
OfflineProxies: totalProxies - onlineProxies,
|
||||||
Proxies: []RelayProxyStat{},
|
Proxies: proxies,
|
||||||
TotalConnections: maxInt(totalConnections, 0),
|
TotalConnections: maxInt(totalConnections, 0),
|
||||||
ClientCounts: 0,
|
ClientCounts: maxInt(clientCounts, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -16,26 +17,29 @@ import (
|
|||||||
|
|
||||||
// RelayHeartbeatPayload is the payload sent by OpenFlareRelay in each heartbeat.
|
// RelayHeartbeatPayload is the payload sent by OpenFlareRelay in each heartbeat.
|
||||||
type RelayHeartbeatPayload struct {
|
type RelayHeartbeatPayload struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ExtVersion string `json:"frp_version"`
|
ExtVersion string `json:"frp_version"`
|
||||||
RelayStatus string `json:"relay_status"`
|
RelayStatus string `json:"relay_status"`
|
||||||
FrpsConnCount int `json:"frps_connections"`
|
FrpsConnCount int `json:"frps_connections"`
|
||||||
FrpsProxyCount int `json:"frps_proxy_count"`
|
FrpsProxyCount int `json:"frps_proxy_count"`
|
||||||
Name string `json:"name"`
|
FrpsClientCount int `json:"frps_client_count"`
|
||||||
IP string `json:"ip"`
|
FrpsProxies []RelayProxyStat `json:"frps_proxies,omitempty"`
|
||||||
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
|
Name string `json:"name"`
|
||||||
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
|
IP string `json:"ip"`
|
||||||
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
|
Profile *AgentNodeSystemProfile `json:"profile,omitempty"`
|
||||||
|
Snapshot *AgentNodeMetricSnapshot `json:"snapshot,omitempty"`
|
||||||
|
HealthEvents []AgentNodeHealthEvent `json:"health_events,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const relayFrpsUnhealthyEventType = "frps_unhealthy"
|
const relayFrpsUnhealthyEventType = "frps_unhealthy"
|
||||||
|
|
||||||
// RelayConfig is the frps configuration sent to the Relay.
|
// RelayConfig is the frps configuration sent to the Relay.
|
||||||
type RelayConfig struct {
|
type RelayConfig struct {
|
||||||
BindPort int `json:"bind_port"`
|
BindPort int `json:"bind_port"`
|
||||||
VhostHTTPPort int `json:"vhost_http_port"`
|
VhostHTTPPort int `json:"vhost_http_port"`
|
||||||
AuthToken string `json:"auth_token"`
|
AuthToken string `json:"auth_token"`
|
||||||
LogLevel string `json:"log_level"`
|
LogLevel string `json:"log_level"`
|
||||||
|
WebServerEnabled bool `json:"web_server_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelaySettings contains runtime settings for the Relay.
|
// RelaySettings contains runtime settings for the Relay.
|
||||||
@@ -148,11 +152,20 @@ func persistRelayHeartbeatObservability(nodeID string, payload RelayHeartbeatPay
|
|||||||
HealthEvents: payload.HealthEvents,
|
HealthEvents: payload.HealthEvents,
|
||||||
}, reportedAt)
|
}, reportedAt)
|
||||||
|
|
||||||
|
var proxiesJSON string
|
||||||
|
if len(payload.FrpsProxies) > 0 {
|
||||||
|
if data, err := json.Marshal(payload.FrpsProxies); err == nil {
|
||||||
|
proxiesJSON = string(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frpsObs := &model.NodeObservationFrps{
|
frpsObs := &model.NodeObservationFrps{
|
||||||
NodeID: nodeID,
|
NodeID: nodeID,
|
||||||
CapturedAt: reportedAt,
|
CapturedAt: reportedAt,
|
||||||
FrpsConnections: payload.FrpsConnCount,
|
FrpsConnections: payload.FrpsConnCount,
|
||||||
FrpsProxyCount: payload.FrpsProxyCount,
|
FrpsProxyCount: payload.FrpsProxyCount,
|
||||||
|
FrpsClientCount: payload.FrpsClientCount,
|
||||||
|
FrpsProxies: proxiesJSON,
|
||||||
}
|
}
|
||||||
_ = frpsObs.Insert()
|
_ = frpsObs.Insert()
|
||||||
}
|
}
|
||||||
@@ -162,10 +175,11 @@ func buildRelayConfig(node *model.Node) *RelayConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &RelayConfig{
|
return &RelayConfig{
|
||||||
BindPort: node.RelayBindPort,
|
BindPort: node.RelayBindPort,
|
||||||
VhostHTTPPort: node.RelayVhostHTTPPort,
|
VhostHTTPPort: node.RelayVhostHTTPPort,
|
||||||
AuthToken: node.RelayAuthToken,
|
AuthToken: node.RelayAuthToken,
|
||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
|
WebServerEnabled: node.RelayWebServerEnabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const nodeEditorSchema = z
|
|||||||
relay_client_access_addr: z.string().trim(),
|
relay_client_access_addr: z.string().trim(),
|
||||||
relay_agent_access_addr: z.string().trim(),
|
relay_agent_access_addr: z.string().trim(),
|
||||||
relay_client_proxy_url: z.string().trim(),
|
relay_client_proxy_url: z.string().trim(),
|
||||||
|
relay_web_server_enabled: z.boolean(),
|
||||||
geo_manual_override: z.boolean(),
|
geo_manual_override: z.boolean(),
|
||||||
geo_region: z.string(),
|
geo_region: z.string(),
|
||||||
geo_name: z.string().trim().max(128, '位置名不能超过 128 个字符'),
|
geo_name: z.string().trim().max(128, '位置名不能超过 128 个字符'),
|
||||||
@@ -142,6 +143,7 @@ const defaultValues: NodeEditorValues = {
|
|||||||
relay_client_access_addr: '',
|
relay_client_access_addr: '',
|
||||||
relay_agent_access_addr: '',
|
relay_agent_access_addr: '',
|
||||||
relay_client_proxy_url: '',
|
relay_client_proxy_url: '',
|
||||||
|
relay_web_server_enabled: false,
|
||||||
geo_manual_override: false,
|
geo_manual_override: false,
|
||||||
geo_region: '',
|
geo_region: '',
|
||||||
geo_name: '',
|
geo_name: '',
|
||||||
@@ -233,6 +235,7 @@ function buildFormValues(node?: Partial<NodeItem> | null): NodeEditorValues {
|
|||||||
relay_client_access_addr: node.relay_client_access_addr ?? '',
|
relay_client_access_addr: node.relay_client_access_addr ?? '',
|
||||||
relay_agent_access_addr: node.relay_agent_access_addr ?? '',
|
relay_agent_access_addr: node.relay_agent_access_addr ?? '',
|
||||||
relay_client_proxy_url: node.relay_client_proxy_url ?? '',
|
relay_client_proxy_url: node.relay_client_proxy_url ?? '',
|
||||||
|
relay_web_server_enabled: node.relay_web_server_enabled ?? false,
|
||||||
geo_manual_override: node.geo_manual_override ?? false,
|
geo_manual_override: node.geo_manual_override ?? false,
|
||||||
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
|
geo_region: node.geo_manual_override ? (node.geo_name ?? '') : '',
|
||||||
geo_name: node.geo_name ?? '',
|
geo_name: node.geo_name ?? '',
|
||||||
@@ -259,6 +262,7 @@ function toPayload(values: NodeEditorValues): NodeMutationPayload {
|
|||||||
relay_client_access_addr: values.type === 'tunnel_relay' ? values.relay_client_access_addr.trim() : undefined,
|
relay_client_access_addr: values.type === 'tunnel_relay' ? values.relay_client_access_addr.trim() : undefined,
|
||||||
relay_agent_access_addr: values.type === 'tunnel_relay' ? values.relay_agent_access_addr.trim() : undefined,
|
relay_agent_access_addr: values.type === 'tunnel_relay' ? values.relay_agent_access_addr.trim() : undefined,
|
||||||
relay_client_proxy_url: values.type === 'tunnel_relay' ? values.relay_client_proxy_url.trim() : undefined,
|
relay_client_proxy_url: values.type === 'tunnel_relay' ? values.relay_client_proxy_url.trim() : undefined,
|
||||||
|
relay_web_server_enabled: values.type === 'tunnel_relay' ? values.relay_web_server_enabled : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!values.geo_manual_override) {
|
if (!values.geo_manual_override) {
|
||||||
@@ -426,6 +430,18 @@ export function NodeEditorModal({
|
|||||||
>
|
>
|
||||||
<ResourceInput placeholder="例如: http://10.0.0.1:3128" {...form.register('relay_client_proxy_url')} />
|
<ResourceInput placeholder="例如: http://10.0.0.1:3128" {...form.register('relay_client_proxy_url')} />
|
||||||
</ResourceField>
|
</ResourceField>
|
||||||
|
|
||||||
|
<ToggleField
|
||||||
|
label="开启 FRPS WebUI"
|
||||||
|
description="开启后可以通过浏览器直接访问 FRPS 的 Web 界面。安全起见,访问密码固定为当前中继节点的接入 Server Token。"
|
||||||
|
checked={form.watch('relay_web_server_enabled')}
|
||||||
|
onChange={(checked) =>
|
||||||
|
form.setValue('relay_web_server_enabled', checked, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldValidate: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -866,6 +866,21 @@ export function RelayDetailPage({ node }: { node: NodeItem }) {
|
|||||||
<p>Relay 中继版本:{node.version || 'unknown'}</p>
|
<p>Relay 中继版本:{node.version || 'unknown'}</p>
|
||||||
<p>frps 核心版本:{node.ext_version || 'unknown'}</p>
|
<p>frps 核心版本:{node.ext_version || 'unknown'}</p>
|
||||||
<p>中继网络接入:{node.relay_agent_access_addr || '—'}</p>
|
<p>中继网络接入:{node.relay_agent_access_addr || '—'}</p>
|
||||||
|
{node.relay_web_server_enabled ? (
|
||||||
|
<p>
|
||||||
|
FRPS WebUI:
|
||||||
|
<a
|
||||||
|
href={`http://${node.ip || '127.0.0.1'}:${node.relay_bind_port + 500}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="text-[var(--accent-strong)] hover:underline font-medium"
|
||||||
|
>
|
||||||
|
点击打开 Web 界面
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p>FRPS WebUI:已禁用</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface NodeItem {
|
|||||||
relay_client_proxy_url: string;
|
relay_client_proxy_url: string;
|
||||||
relay_auth_token: string;
|
relay_auth_token: string;
|
||||||
relay_status: string;
|
relay_status: string;
|
||||||
|
relay_web_server_enabled: boolean;
|
||||||
relay_frps_connections: number;
|
relay_frps_connections: number;
|
||||||
relay_frps_proxy_count: number;
|
relay_frps_proxy_count: number;
|
||||||
geo_name: string;
|
geo_name: string;
|
||||||
@@ -59,6 +60,7 @@ export interface NodeMutationPayload {
|
|||||||
relay_client_access_addr?: string;
|
relay_client_access_addr?: string;
|
||||||
relay_agent_access_addr?: string;
|
relay_agent_access_addr?: string;
|
||||||
relay_client_proxy_url?: string;
|
relay_client_proxy_url?: string;
|
||||||
|
relay_web_server_enabled?: boolean;
|
||||||
auto_update_enabled: boolean;
|
auto_update_enabled: boolean;
|
||||||
geo_name: string;
|
geo_name: string;
|
||||||
geo_latitude?: number | null;
|
geo_latitude?: number | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user