[优化] 增加 FRPS WebUI 支持,添加相关配置和数据库迁移

This commit is contained in:
ryan
2026-06-01 21:59:42 +08:00
parent d6e7e2baa2
commit fefe205158
14 changed files with 352 additions and 42 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ func main() {
stateStore := state.NewStore(cfg.StatePath)
_ = 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())
+155 -8
View File
@@ -3,8 +3,10 @@ package frps
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"os/exec"
"path/filepath"
@@ -19,6 +21,7 @@ type Manager struct {
frpsPath string
dataDir string
configPath string
agentToken string
mu sync.RWMutex
activeConfig *service.RelayConfig
@@ -34,15 +37,18 @@ type RuntimeStatus struct {
LastError string
Connections int
ProxyCount int
ClientCount int
Proxies []service.RelayProxyStat
ProcessAlive bool
}
func NewManager(frpsPath string, dataDir string) *Manager {
func NewManager(frpsPath string, dataDir string, agentToken string) *Manager {
return &Manager{
frpsPath: frpsPath,
dataDir: dataDir,
configPath: filepath.Join(dataDir, "frps.toml"),
status: "unhealthy",
agentToken: agentToken,
}
}
@@ -65,15 +71,139 @@ func (m *Manager) GetStatus() string {
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 {
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{
Status: m.status,
LastError: m.lastError,
Connections: 0,
ProxyCount: 0,
ProcessAlive: m.cmd != nil && m.cmd.Process != nil,
Status: status,
LastError: lastError,
Connections: connections,
ProxyCount: proxyCount,
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 &&
m.activeConfig.BindPort == cfg.BindPort &&
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 {
slog.Warn("frps config unchanged but process is not running, restarting")
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))
}
// 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)
}
+12 -10
View File
@@ -54,16 +54,18 @@ func (s *Service) doHeartbeat(ctx context.Context) {
runtimeStatus := s.frpsManager.GetRuntimeStatus()
payload := service.RelayHeartbeatPayload{
Version: config.Version,
ExtVersion: s.frpsManager.GetVersion(),
RelayStatus: runtimeStatus.Status,
FrpsConnCount: runtimeStatus.Connections,
FrpsProxyCount: runtimeStatus.ProxyCount,
Name: s.config.NodeName,
IP: s.config.NodeIP,
Profile: observability.BuildProfile(s.config, s.stateStore),
Snapshot: observability.BuildSnapshot(s.config, s.stateStore),
HealthEvents: observability.BuildHealthEvents(runtimeStatus),
Version: config.Version,
ExtVersion: s.frpsManager.GetVersion(),
RelayStatus: runtimeStatus.Status,
FrpsConnCount: runtimeStatus.Connections,
FrpsProxyCount: runtimeStatus.ProxyCount,
FrpsClientCount: runtimeStatus.ClientCount,
FrpsProxies: runtimeStatus.Proxies,
Name: s.config.NodeName,
IP: s.config.NodeIP,
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)