mirror of
https://github.com/fatedier/frp.git
synced 2026-10-20 18:53:27 +08:00
Add pkg/metrics/text, a small Prometheus text-format writer, and use it for frpc's /metrics. The endpoint lives only on metricsServer and is covered by its Basic Auth, like the other endpoints there. The package emits the Go runtime families and, on Linux, the process families read from /proc. Their names, types and help strings match client_golang's default collectors, so existing Go runtime dashboards keep working. It deliberately emits no frp_ business metrics and none of client_golang's promhttp_ handler metrics. Writing the subset ourselves keeps client_golang and protobuf out of the frpc binary; both are still used by frps and by the tests here.
622 lines
18 KiB
Go
622 lines
18 KiB
Go
// Copyright 2017 fatedier, fatedier@gmail.com
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/fatedier/golib/crypto"
|
|
"github.com/samber/lo"
|
|
|
|
"github.com/fatedier/frp/client/proxy"
|
|
"github.com/fatedier/frp/pkg/auth"
|
|
"github.com/fatedier/frp/pkg/config"
|
|
"github.com/fatedier/frp/pkg/config/source"
|
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
"github.com/fatedier/frp/pkg/metrics/text"
|
|
"github.com/fatedier/frp/pkg/msg"
|
|
"github.com/fatedier/frp/pkg/policy/security"
|
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
"github.com/fatedier/frp/pkg/util/log"
|
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
|
"github.com/fatedier/frp/pkg/util/wait"
|
|
"github.com/fatedier/frp/pkg/util/xlog"
|
|
"github.com/fatedier/frp/pkg/vnet"
|
|
)
|
|
|
|
func init() {
|
|
crypto.DefaultSalt = "frp"
|
|
// Disable quic-go's receive buffer warning.
|
|
os.Setenv("QUIC_GO_DISABLE_RECEIVE_BUFFER_WARNING", "true")
|
|
// Disable quic-go's ECN support by default. It may cause issues on certain operating systems.
|
|
if os.Getenv("QUIC_GO_DISABLE_ECN") == "" {
|
|
os.Setenv("QUIC_GO_DISABLE_ECN", "true")
|
|
}
|
|
}
|
|
|
|
type cancelErr struct {
|
|
Err error
|
|
}
|
|
|
|
func (e cancelErr) Error() string {
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// ServiceOptions contains options for creating a new client service.
|
|
type ServiceOptions struct {
|
|
Common *v1.ClientCommonConfig
|
|
|
|
// ConfigSourceAggregator manages internal config and optional store sources.
|
|
// It is required for creating a Service.
|
|
ConfigSourceAggregator *source.Aggregator
|
|
|
|
UnsafeFeatures *security.UnsafeFeatures
|
|
|
|
// ConfigFilePath is the path to the configuration file used to initialize.
|
|
// If it is empty, it means that the configuration file is not used for initialization.
|
|
// It may be initialized using command line parameters or called directly.
|
|
ConfigFilePath string
|
|
|
|
// ClientSpec is the client specification that control the client behavior.
|
|
ClientSpec *msg.ClientSpec
|
|
|
|
// ConnectorCreator is a function that creates a new connector to make connections to the server.
|
|
// The Connector shields the underlying connection details, whether it is through TCP or QUIC connection,
|
|
// and regardless of whether multiplexing is used.
|
|
//
|
|
// If it is not set, the default frpc connector will be used.
|
|
// By using a custom Connector, it can be used to implement a VirtualClient, which connects to frps
|
|
// through a pipe instead of a real physical connection.
|
|
ConnectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
|
|
|
// HandleWorkConnCb is a callback function that is called when a new work connection is created.
|
|
//
|
|
// If it is not set, the default frpc implementation will be used.
|
|
HandleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
|
}
|
|
|
|
// setServiceOptionsDefault sets the default values for ServiceOptions.
|
|
func setServiceOptionsDefault(options *ServiceOptions) error {
|
|
if options.Common != nil {
|
|
if err := options.Common.Complete(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if options.ConnectorCreator == nil {
|
|
options.ConnectorCreator = NewConnector
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Service is the client service that connects to frps and provides proxy services.
|
|
type Service struct {
|
|
ctlMu sync.RWMutex
|
|
// Stores gracefulShutdownDuration independently from ctlMu, because the
|
|
// graceful shutdown wait may hold ctlMu for an arbitrary duration.
|
|
gracefulShutdownDuration atomic.Int64
|
|
// manager control connection with server
|
|
ctl *Control
|
|
// Uniq id got from frps, it will be attached to loginMsg.
|
|
runID string
|
|
|
|
// Auth runtime and encryption materials
|
|
auth *auth.ClientAuth
|
|
|
|
// web server for admin UI and apis
|
|
webServer *httppkg.Server
|
|
metricsServer *httppkg.Server
|
|
|
|
vnetController *vnet.Controller
|
|
|
|
cfgMu sync.RWMutex
|
|
// reloadMu serializes config updates with control startup and publication,
|
|
// keeping reloadCommon and applied config in sync across reloads and reconnects.
|
|
reloadMu sync.Mutex
|
|
common *v1.ClientCommonConfig
|
|
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
|
// It can be updated by /api/reload without mutating startup-only common behavior.
|
|
reloadCommon *v1.ClientCommonConfig
|
|
proxyCfgs []v1.ProxyConfigurer
|
|
visitorCfgs []v1.VisitorConfigurer
|
|
clientSpec *msg.ClientSpec
|
|
|
|
// aggregator manages multiple configuration sources.
|
|
// When set, the service watches for config changes and reloads automatically.
|
|
aggregator *source.Aggregator
|
|
configSource *source.ConfigSource
|
|
storeSource *source.StoreSource
|
|
|
|
unsafeFeatures *security.UnsafeFeatures
|
|
|
|
// The configuration file used to initialize this client, or an empty
|
|
// string if no configuration file was used.
|
|
configFilePath string
|
|
|
|
// service context
|
|
ctx context.Context
|
|
// call cancel to stop service
|
|
cancel context.CancelCauseFunc
|
|
// Protect cancel publication and shutdown before the first Run.
|
|
lifecycleMu sync.Mutex
|
|
closed bool
|
|
|
|
connectorCreator func(context.Context, *v1.ClientCommonConfig) Connector
|
|
handleWorkConnCb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool
|
|
}
|
|
|
|
func NewService(options ServiceOptions) (*Service, error) {
|
|
if err := setServiceOptionsDefault(&options); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if options.ConfigSourceAggregator == nil {
|
|
return nil, fmt.Errorf("config source aggregator is required")
|
|
}
|
|
|
|
configSource := options.ConfigSourceAggregator.ConfigSource()
|
|
storeSource := options.ConfigSourceAggregator.StoreSource()
|
|
|
|
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
|
if loadErr != nil {
|
|
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
|
}
|
|
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
|
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
|
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
|
|
|
// Create HTTP servers after all other fallible steps, rolling back the web
|
|
// listener if the metrics server cannot be created.
|
|
var webServer *httppkg.Server
|
|
if options.Common.WebServer.Port > 0 {
|
|
ws, err := httppkg.NewServer(options.Common.WebServer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
webServer = ws
|
|
}
|
|
// frpc exposes Go runtime metrics only. They are written by pkg/metrics/text
|
|
// so that client_golang and protobuf stay out of the frpc binary.
|
|
var metricsHandler http.Handler
|
|
if options.Common.MetricsServer.Port > 0 {
|
|
registry := text.NewRegistry()
|
|
registry.RegisterRuntime()
|
|
metricsHandler = registry.Handler()
|
|
}
|
|
metricsServer, err := httppkg.NewMetricsServer(options.Common.MetricsServer, metricsHandler)
|
|
if err != nil {
|
|
if webServer != nil {
|
|
_ = webServer.Close()
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
s := &Service{
|
|
ctx: context.Background(),
|
|
auth: authRuntime,
|
|
webServer: webServer,
|
|
metricsServer: metricsServer,
|
|
common: options.Common,
|
|
reloadCommon: options.Common,
|
|
configFilePath: options.ConfigFilePath,
|
|
unsafeFeatures: options.UnsafeFeatures,
|
|
proxyCfgs: proxyCfgs,
|
|
visitorCfgs: visitorCfgs,
|
|
clientSpec: options.ClientSpec,
|
|
aggregator: options.ConfigSourceAggregator,
|
|
configSource: configSource,
|
|
storeSource: storeSource,
|
|
connectorCreator: options.ConnectorCreator,
|
|
handleWorkConnCb: options.HandleWorkConnCb,
|
|
}
|
|
|
|
if webServer != nil {
|
|
webServer.RouteRegister(s.registerRouteHandlers)
|
|
}
|
|
if options.Common.VirtualNet.Address != "" {
|
|
s.vnetController = vnet.NewController(options.Common.VirtualNet)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (svr *Service) Run(ctx context.Context) error {
|
|
ctx, cancel := context.WithCancelCause(ctx)
|
|
svr.lifecycleMu.Lock()
|
|
if svr.closed {
|
|
svr.lifecycleMu.Unlock()
|
|
cancel(nil)
|
|
return ctx.Err()
|
|
}
|
|
svr.ctx = xlog.NewContext(ctx, xlog.FromContextSafe(ctx))
|
|
svr.cancel = cancel
|
|
svr.lifecycleMu.Unlock()
|
|
defer cancel(nil)
|
|
defer svr.stop()
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// set custom DNSServer
|
|
if svr.common.DNSServer != "" {
|
|
netpkg.SetDefaultDNSAddress(svr.common.DNSServer)
|
|
}
|
|
|
|
if svr.vnetController != nil {
|
|
vnetController := svr.vnetController
|
|
if err := svr.vnetController.Init(); err != nil {
|
|
log.Errorf("init virtual network controller error: %v", err)
|
|
return err
|
|
}
|
|
go func() {
|
|
log.Infof("virtual network controller start...")
|
|
if err := vnetController.Run(); err != nil && !errors.Is(err, net.ErrClosed) {
|
|
log.Warnf("virtual network controller exit with error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if svr.webServer != nil {
|
|
if svr.common.WebServer.PprofEnable {
|
|
log.Warnf("webServer pprof endpoints are deprecated; use metricsServer.pprofEnable instead")
|
|
}
|
|
if svr.common.MetricsServer.Port == 0 {
|
|
log.Infof("webServer /healthz is deprecated; health probes can migrate to metricsServer.port")
|
|
}
|
|
webServer := svr.webServer
|
|
go func() {
|
|
log.Infof("admin server listen on %s", webServer.Address())
|
|
if err := webServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Warnf("admin server exit with error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if svr.metricsServer != nil {
|
|
metricsServer := svr.metricsServer
|
|
go func() {
|
|
log.Infof("metrics server listen on %s", metricsServer.Address())
|
|
if err := metricsServer.Run(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
log.Warnf("metrics server exit with error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// first login to frps
|
|
svr.loopLoginUntilSuccess(10*time.Second, lo.FromPtr(svr.common.LoginFailExit))
|
|
if svr.ctl == nil {
|
|
cancelCause := cancelErr{}
|
|
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
|
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
|
}
|
|
|
|
go svr.keepControllerWorking()
|
|
|
|
<-svr.ctx.Done()
|
|
return nil
|
|
}
|
|
|
|
func (svr *Service) keepControllerWorking() {
|
|
// ctl is swapped under ctlMu in loopLoginUntilSuccess / stop; take a
|
|
// snapshot before waiting so stop's nil write does not race this read.
|
|
svr.ctlMu.RLock()
|
|
ctl := svr.ctl
|
|
svr.ctlMu.RUnlock()
|
|
if ctl == nil {
|
|
return
|
|
}
|
|
<-ctl.Done()
|
|
|
|
// There is a situation where the login is successful but due to certain reasons,
|
|
// the control immediately exits. It is necessary to limit the frequency of reconnection in this case.
|
|
// The interval for the first three retries in 1 minute will be very short, and then it will increase exponentially.
|
|
// The maximum interval is 20 seconds.
|
|
wait.BackoffUntil(func() (bool, error) {
|
|
// loopLoginUntilSuccess is another layer of loop that will continuously attempt to
|
|
// login to the server until successful.
|
|
svr.loopLoginUntilSuccess(20*time.Second, false)
|
|
svr.ctlMu.RLock()
|
|
ctl := svr.ctl
|
|
svr.ctlMu.RUnlock()
|
|
if ctl != nil {
|
|
<-ctl.Done()
|
|
return false, errors.New("control is closed and try another loop")
|
|
}
|
|
// If the control is nil, it means that the login failed and the service is also closed.
|
|
return false, nil
|
|
}, wait.NewFastBackoffManager(
|
|
wait.FastBackoffOptions{
|
|
Duration: time.Second,
|
|
Factor: 2,
|
|
Jitter: 0.1,
|
|
MaxDuration: 20 * time.Second,
|
|
FastRetryCount: 3,
|
|
FastRetryDelay: 200 * time.Millisecond,
|
|
FastRetryWindow: time.Minute,
|
|
FastRetryJitter: 0.5,
|
|
},
|
|
), true, svr.ctx.Done())
|
|
}
|
|
|
|
func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
|
|
xl := xlog.FromContextSafe(svr.ctx)
|
|
|
|
loginFunc := func() (bool, error) {
|
|
xl.Infof("try to connect to server...")
|
|
dialer := &controlSessionDialer{
|
|
ctx: svr.ctx,
|
|
common: svr.common,
|
|
auth: svr.auth,
|
|
clientSpec: svr.clientSpec,
|
|
vnetController: svr.vnetController,
|
|
connectorCreator: svr.connectorCreator,
|
|
}
|
|
sessionCtx, err := dialer.Dial(svr.runID)
|
|
if err != nil {
|
|
xl.Warnf("connect to server error: %v", err)
|
|
if firstLoginExit {
|
|
svr.cancel(cancelErr{Err: err})
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
svr.runID = sessionCtx.RunID
|
|
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
|
xl.Infof("login to server success, get run id [%s]", svr.runID)
|
|
|
|
ctl, err := NewControl(svr.ctx, sessionCtx)
|
|
if err != nil {
|
|
sessionCtx.Conn.Close()
|
|
sessionCtx.Connector.Close()
|
|
xl.Errorf("new control error: %v", err)
|
|
return false, err
|
|
}
|
|
ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
|
|
|
|
// An update must either precede this snapshot or apply to the published
|
|
// control. Keep dialing and login outside the reload transaction.
|
|
svr.reloadMu.Lock()
|
|
defer svr.reloadMu.Unlock()
|
|
svr.cfgMu.RLock()
|
|
proxyCfgs := svr.proxyCfgs
|
|
visitorCfgs := svr.visitorCfgs
|
|
svr.cfgMu.RUnlock()
|
|
|
|
ctl.Run(proxyCfgs, visitorCfgs)
|
|
// close and replace previous control
|
|
svr.ctlMu.Lock()
|
|
if svr.ctl != nil {
|
|
svr.ctl.Close()
|
|
}
|
|
svr.ctl = ctl
|
|
svr.ctlMu.Unlock()
|
|
return true, nil
|
|
}
|
|
|
|
// try to reconnect to server until success
|
|
wait.BackoffUntil(loginFunc, wait.NewFastBackoffManager(
|
|
wait.FastBackoffOptions{
|
|
Duration: time.Second,
|
|
Factor: 2,
|
|
Jitter: 0.1,
|
|
MaxDuration: maxInterval,
|
|
}), true, svr.ctx.Done())
|
|
}
|
|
|
|
func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
|
svr.reloadMu.Lock()
|
|
defer svr.reloadMu.Unlock()
|
|
return svr.updateAllConfigurerLocked(proxyCfgs, visitorCfgs)
|
|
}
|
|
|
|
// Caller must hold reloadMu.
|
|
func (svr *Service) updateAllConfigurerLocked(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
|
|
svr.cfgMu.Lock()
|
|
svr.proxyCfgs = proxyCfgs
|
|
svr.visitorCfgs = visitorCfgs
|
|
svr.cfgMu.Unlock()
|
|
|
|
svr.ctlMu.RLock()
|
|
ctl := svr.ctl
|
|
svr.ctlMu.RUnlock()
|
|
|
|
if ctl != nil {
|
|
return ctl.UpdateAllConfigurer(proxyCfgs, visitorCfgs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (svr *Service) UpdateConfigSource(
|
|
common *v1.ClientCommonConfig,
|
|
proxyCfgs []v1.ProxyConfigurer,
|
|
visitorCfgs []v1.VisitorConfigurer,
|
|
) error {
|
|
svr.reloadMu.Lock()
|
|
defer svr.reloadMu.Unlock()
|
|
|
|
return svr.updateConfigSourceLocked(common, proxyCfgs, visitorCfgs)
|
|
}
|
|
|
|
// Caller must hold reloadMu.
|
|
func (svr *Service) updateConfigSourceLocked(
|
|
common *v1.ClientCommonConfig,
|
|
proxyCfgs []v1.ProxyConfigurer,
|
|
visitorCfgs []v1.VisitorConfigurer,
|
|
) error {
|
|
cfgSource := svr.configSource
|
|
if cfgSource == nil {
|
|
return fmt.Errorf("config source is not available")
|
|
}
|
|
|
|
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Non-atomic update semantics: source has been updated at this point.
|
|
// Even if reload fails below, keep this common config for subsequent reloads.
|
|
svr.cfgMu.Lock()
|
|
svr.reloadCommon = common
|
|
svr.cfgMu.Unlock()
|
|
|
|
if err := svr.reloadConfigFromSourcesLocked(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (svr *Service) Close() {
|
|
svr.GracefulClose(time.Duration(0))
|
|
}
|
|
|
|
func (svr *Service) GracefulClose(d time.Duration) {
|
|
svr.gracefulShutdownDuration.Store(int64(d))
|
|
svr.lifecycleMu.Lock()
|
|
if svr.closed {
|
|
svr.lifecycleMu.Unlock()
|
|
return
|
|
}
|
|
svr.closed = true
|
|
cancel := svr.cancel
|
|
svr.lifecycleMu.Unlock()
|
|
if cancel != nil {
|
|
// Once Run has claimed startup, it also owns cleanup.
|
|
cancel(nil)
|
|
} else {
|
|
svr.stop()
|
|
}
|
|
}
|
|
|
|
func (svr *Service) stop() {
|
|
// Coordinate shutdown with reload/update paths that read source pointers.
|
|
svr.reloadMu.Lock()
|
|
if svr.aggregator != nil {
|
|
svr.aggregator = nil
|
|
}
|
|
svr.configSource = nil
|
|
svr.storeSource = nil
|
|
svr.reloadMu.Unlock()
|
|
|
|
svr.ctlMu.Lock()
|
|
defer svr.ctlMu.Unlock()
|
|
if svr.ctl != nil {
|
|
d := time.Duration(svr.gracefulShutdownDuration.Load())
|
|
svr.ctl.GracefulClose(d)
|
|
svr.ctl = nil
|
|
}
|
|
if svr.webServer != nil {
|
|
svr.webServer.Close()
|
|
svr.webServer = nil
|
|
}
|
|
if svr.metricsServer != nil {
|
|
svr.metricsServer.Close()
|
|
svr.metricsServer = nil
|
|
}
|
|
if svr.vnetController != nil {
|
|
_ = svr.vnetController.Stop()
|
|
svr.vnetController = nil
|
|
}
|
|
}
|
|
|
|
func (svr *Service) getProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
|
svr.ctlMu.RLock()
|
|
ctl := svr.ctl
|
|
svr.ctlMu.RUnlock()
|
|
|
|
if ctl == nil {
|
|
return nil, false
|
|
}
|
|
return ctl.pm.GetProxyStatus(name)
|
|
}
|
|
|
|
func (svr *Service) getVisitorCfg(name string) (v1.VisitorConfigurer, bool) {
|
|
svr.ctlMu.RLock()
|
|
ctl := svr.ctl
|
|
svr.ctlMu.RUnlock()
|
|
|
|
if ctl == nil {
|
|
return nil, false
|
|
}
|
|
return ctl.vm.GetVisitorCfg(name)
|
|
}
|
|
|
|
func (svr *Service) StatusExporter() StatusExporter {
|
|
return &statusExporterImpl{
|
|
getProxyStatusFunc: svr.getProxyStatus,
|
|
}
|
|
}
|
|
|
|
type StatusExporter interface {
|
|
GetProxyStatus(name string) (*proxy.WorkingStatus, bool)
|
|
}
|
|
|
|
type statusExporterImpl struct {
|
|
getProxyStatusFunc func(name string) (*proxy.WorkingStatus, bool)
|
|
}
|
|
|
|
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
|
return s.getProxyStatusFunc(name)
|
|
}
|
|
|
|
func (svr *Service) reloadConfigFromSources() error {
|
|
svr.reloadMu.Lock()
|
|
defer svr.reloadMu.Unlock()
|
|
return svr.reloadConfigFromSourcesLocked()
|
|
}
|
|
|
|
func (svr *Service) reloadConfigFromSourcesLocked() error {
|
|
aggregator := svr.aggregator
|
|
if aggregator == nil {
|
|
return errors.New("config aggregator is not initialized")
|
|
}
|
|
|
|
svr.cfgMu.RLock()
|
|
reloadCommon := svr.reloadCommon
|
|
svr.cfgMu.RUnlock()
|
|
|
|
proxies, visitors, err := aggregator.Load()
|
|
if err != nil {
|
|
return fmt.Errorf("reload config from sources failed: %w", err)
|
|
}
|
|
|
|
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
|
proxies = config.CompleteProxyConfigurers(proxies)
|
|
visitors = config.CompleteVisitorConfigurers(visitors)
|
|
requirements := validation.GetClientConfigRequirements(reloadCommon, proxies, visitors)
|
|
if svr.vnetController == nil && requirements.VirtualNet {
|
|
return errors.New(
|
|
"VirtualNet-dependent configuration requires a VirtualNet runtime enabled at startup; " +
|
|
"restart frpc after configuring featureGates.VirtualNet and virtualNet.address",
|
|
)
|
|
}
|
|
|
|
// Atomically replace the entire configuration
|
|
if err := svr.updateAllConfigurerLocked(proxies, visitors); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|