mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 08:57:55 +00:00
feat: M3 load balancing + health check (model/render/admin API status/UI)
This commit is contained in:
@ -39,6 +39,19 @@ type Proxy struct {
|
||||
HTTPHeaders map[string]string // additional request headers
|
||||
BasicAuthUser string
|
||||
BasicAuthPassword string
|
||||
|
||||
// M3 Load balancing + health check.
|
||||
// LBGroup/LBGroupKey put this proxy into a frps load-balancing group;
|
||||
// all members with the same group are balanced across.
|
||||
LBGroup string
|
||||
LBGroupKey string
|
||||
|
||||
// HealthCheck* enable frpc's built-in health monitor. Empty type = off.
|
||||
HealthCheckType string // "" | tcp | http
|
||||
HealthCheckPath string // http only; path to GET on LocalIP:LocalPort
|
||||
HealthCheckTimeout int // seconds, default 3
|
||||
HealthCheckMaxFailed int // default 1
|
||||
HealthCheckInterval int // seconds, default 10
|
||||
}
|
||||
|
||||
// frpcAuth mirrors frpc's auth section.
|
||||
@ -96,16 +109,52 @@ type frpcProxy struct {
|
||||
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
||||
HTTPHeaders map[string]string `json:"httpHeaders,omitempty"`
|
||||
HTTPBasicAuth *frpcBasicAuth `json:"basicAuth,omitempty"`
|
||||
|
||||
// M3 load balancer group + health check.
|
||||
LoadBalancer *frpcLoadBalancer `json:"loadBalancer,omitempty"`
|
||||
HealthCheck *frpcHealthCheck `json:"healthCheck,omitempty"`
|
||||
}
|
||||
|
||||
// frpcWebServer is the frpc admin API (webServer) section. Enabled when
|
||||
// AdminPort > 0; used by the manager to query true per-proxy status.
|
||||
type frpcWebServer struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Port int `json:"port,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
// frpcLoadBalancer mirrors frpc's loadBalancer section (group load balancing).
|
||||
type frpcLoadBalancer struct {
|
||||
Group string `json:"group"`
|
||||
GroupKey string `json:"groupKey,omitempty"`
|
||||
}
|
||||
|
||||
// frpcHealthCheck mirrors frpc's healthCheck section.
|
||||
type frpcHealthCheck struct {
|
||||
Type string `json:"type"`
|
||||
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
||||
MaxFailed int `json:"maxFailed,omitempty"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
Path string `json:"path,omitempty"`
|
||||
HTTPHeaders []frpcHTTPHeader `json:"httpHeaders,omitempty"`
|
||||
}
|
||||
|
||||
// frpcHTTPHeader is a single extra header for an http health check.
|
||||
type frpcHTTPHeader struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Config is a complete frpc configuration document.
|
||||
type Config struct {
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
Auth frpcAuth `json:"auth"`
|
||||
Transport frpcTransport `json:"transport,omitempty"`
|
||||
LoginFailExit bool `json:"loginFailExit"`
|
||||
Proxies []frpcProxy `json:"proxies"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
Auth frpcAuth `json:"auth"`
|
||||
Transport frpcTransport `json:"transport,omitempty"`
|
||||
WebServer *frpcWebServer `json:"webServer,omitempty"`
|
||||
LoginFailExit bool `json:"loginFailExit"`
|
||||
Proxies []frpcProxy `json:"proxies"`
|
||||
}
|
||||
|
||||
// Render builds the worker config JSON for a remote server from its setting
|
||||
@ -118,6 +167,18 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
|
||||
LoginFailExit: false,
|
||||
Proxies: make([]frpcProxy, 0, len(proxies)),
|
||||
}
|
||||
// M3: optional local frpc admin API (webServer) so the manager can query
|
||||
// true per-proxy status. Only emitted when a port is configured.
|
||||
if remote.AdminPort > 0 {
|
||||
addr := remote.AdminAddr
|
||||
if addr == "" {
|
||||
addr = "127.0.0.1"
|
||||
}
|
||||
cfg.WebServer = &frpcWebServer{
|
||||
Addr: addr, Port: remote.AdminPort,
|
||||
User: remote.AdminUser, Password: remote.AdminPassword,
|
||||
}
|
||||
}
|
||||
if remote.TransportProtocol != "" || remote.TransportTLS || remote.TransportPool > 0 || remote.TransportTLSServerName != "" {
|
||||
cfg.Transport = frpcTransport{
|
||||
Protocol: remote.TransportProtocol,
|
||||
@ -154,6 +215,31 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
|
||||
if p.BasicAuthUser != "" || p.BasicAuthPassword != "" {
|
||||
frpcP.HTTPBasicAuth = &frpcBasicAuth{User: p.BasicAuthUser, Password: p.BasicAuthPassword}
|
||||
}
|
||||
// M3: load balancing group.
|
||||
if p.LBGroup != "" {
|
||||
frpcP.LoadBalancer = &frpcLoadBalancer{Group: p.LBGroup, GroupKey: p.LBGroupKey}
|
||||
}
|
||||
// M3: health check. Type "" means off; frpc defaults apply when a
|
||||
// specific knob is left zero.
|
||||
if p.HealthCheckType != "" {
|
||||
hc := &frpcHealthCheck{
|
||||
Type: p.HealthCheckType,
|
||||
TimeoutSeconds: p.HealthCheckTimeout,
|
||||
MaxFailed: p.HealthCheckMaxFailed,
|
||||
IntervalSeconds: p.HealthCheckInterval,
|
||||
Path: p.HealthCheckPath,
|
||||
}
|
||||
// The HTTP health probe needs its own headers (distinct from the
|
||||
// proxy's request headers): map the proxy's HTTPHeaders through.
|
||||
if len(p.HTTPHeaders) > 0 {
|
||||
hh := make([]frpcHTTPHeader, 0, len(p.HTTPHeaders))
|
||||
for name, val := range p.HTTPHeaders {
|
||||
hh = append(hh, frpcHTTPHeader{Name: name, Value: val})
|
||||
}
|
||||
hc.HTTPHeaders = hh
|
||||
}
|
||||
frpcP.HealthCheck = hc
|
||||
}
|
||||
// http/https proxies use customDomains instead of remotePort. The
|
||||
// default domain is <name>.local; frps matches it by Host header.
|
||||
if p.Type == "http" || p.Type == "https" {
|
||||
|
||||
@ -159,3 +159,108 @@ func TestRenderHTTPSUsesCustomDomains(t *testing.T) {
|
||||
t.Fatalf("customDomains = %+v", p.CustomDomains)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderLoadBalancerAndHealthCheck(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000, Token: "secret"}
|
||||
data, err := Render(remote, []Proxy{
|
||||
{Name: "web1", Type: "tcp", LocalIP: "127.0.0.1", LocalPort: 8080, RemotePort: 8080,
|
||||
LBGroup: "web", LBGroupKey: "k",
|
||||
HealthCheckType: "tcp", HealthCheckTimeout: 3, HealthCheckMaxFailed: 2, HealthCheckInterval: 5},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := cfg.Proxies[0]
|
||||
if p.LoadBalancer == nil || p.LoadBalancer.Group != "web" || p.LoadBalancer.GroupKey != "k" {
|
||||
t.Fatalf("loadBalancer = %+v", p.LoadBalancer)
|
||||
}
|
||||
if p.HealthCheck == nil || p.HealthCheck.Type != "tcp" || p.HealthCheck.IntervalSeconds != 5 ||
|
||||
p.HealthCheck.MaxFailed != 2 || p.HealthCheck.TimeoutSeconds != 3 {
|
||||
t.Fatalf("healthCheck = %+v", p.HealthCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHealthCheckHTTPWithHeaders(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
|
||||
data, err := Render(remote, []Proxy{
|
||||
{Name: "web", Type: "http", LocalIP: "127.0.0.1", LocalPort: 8080,
|
||||
CustomDomains: []string{"app.example.com"},
|
||||
HealthCheckType: "http",
|
||||
HealthCheckPath: "/healthz",
|
||||
HealthCheckTimeout: 2,
|
||||
HealthCheckMaxFailed: 3,
|
||||
HealthCheckInterval: 7,
|
||||
HTTPHeaders: map[string]string{"X-Probe": "1"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
_ = json.Unmarshal(data, &cfg)
|
||||
p := cfg.Proxies[0]
|
||||
if p.HealthCheck == nil || p.HealthCheck.Type != "http" || p.HealthCheck.Path != "/healthz" ||
|
||||
p.HealthCheck.IntervalSeconds != 7 {
|
||||
t.Fatalf("healthCheck = %+v", p.HealthCheck)
|
||||
}
|
||||
if p.HealthCheck.HTTPHeaders == nil || len(p.HealthCheck.HTTPHeaders) == 0 ||
|
||||
p.HealthCheck.HTTPHeaders[0].Name != "X-Probe" || p.HealthCheck.HTTPHeaders[0].Value != "1" {
|
||||
t.Fatalf("healthCheck.httpHeaders = %+v", p.HealthCheck.HTTPHeaders)
|
||||
}
|
||||
// http proxy must still use customDomains and no remotePort
|
||||
if len(p.CustomDomains) != 1 || p.CustomDomains[0] != "app.example.com" || p.RemotePort != 0 {
|
||||
t.Fatalf("http proxy routing = customDomains=%v remotePort=%d", p.CustomDomains, p.RemotePort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHealthCheckOmittedWhenOff(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
|
||||
data, err := Render(remote, []Proxy{
|
||||
{Name: "web", Type: "tcp", LocalIP: "127.0.0.1", LocalPort: 8080, RemotePort: 8080},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
_ = json.Unmarshal(data, &cfg)
|
||||
p := cfg.Proxies[0]
|
||||
if p.LoadBalancer != nil || p.HealthCheck != nil {
|
||||
t.Fatalf("lb/health should be omitted, got lb=%+v hc=%+v", p.LoadBalancer, p.HealthCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderAdminWebServer(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000, Token: "secret",
|
||||
AdminAddr: "127.0.0.1", AdminPort: 7400, AdminUser: "admin", AdminPassword: "pw"}
|
||||
data, err := Render(remote, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.WebServer == nil {
|
||||
t.Fatal("webServer missing when AdminPort set")
|
||||
}
|
||||
if cfg.WebServer.Port != 7400 || cfg.WebServer.Addr != "127.0.0.1" ||
|
||||
cfg.WebServer.User != "admin" || cfg.WebServer.Password != "pw" {
|
||||
t.Fatalf("webServer = %+v", cfg.WebServer)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderAdminWebServerOmitted(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
|
||||
data, err := Render(remote, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
_ = json.Unmarshal(data, &cfg)
|
||||
if cfg.WebServer != nil {
|
||||
t.Fatalf("webServer should be omitted, got %+v", cfg.WebServer)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user