mirror of
https://github.com/fatedier/frp.git
synced 2026-10-19 23:28:08 +08:00
00e4fc4dd3
The https2http and https2https plugins read their certificate and key once when the proxy starts, so a certificate that an external tool renews on disk is only picked up after the proxy itself is recreated. frpc reload rebuilt proxies only when their configuration changed, and a rebuild closes the work connections that are already in use. Let the plugin own the refresh. The HTTPS plugin server keeps the active keypair and the last refresh error in one immutable snapshot published through an atomic pointer, so a TLS handshake only loads that pointer and never touches the disk. Reload rereads both files, and when their bytes match the published pair it returns without parsing or replacing anything; otherwise it parses the pair and swaps the snapshot in. A read or parse failure keeps the previous certificate serving and records the error next to it. frpc reload collects the reloadable plugins of the live proxies and applies the new configuration inside the same reload transaction that also serializes control handoff, so a reload that races a reconnect still refreshes the plugins of the published control. The certificate files are read only after that transaction has been released. The config store API keeps applying configuration without refreshing certificates, because only an explicit reload may reread files. Proxy status reports a refresh failure next to the still running proxy instead of dropping its remote address. No configuration field is added, and frps and the wire protocol are unchanged. Without a reload nothing changes: certificates are still read once at startup, generated self-signed certificates still never reload, and a proxy whose configuration changed still goes through the existing rebuild path.
323 lines
7.9 KiB
Go
323 lines
7.9 KiB
Go
// Copyright 2023 The frp Authors
|
|
//
|
|
// 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 proxy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/fatedier/golib/errors"
|
|
|
|
"github.com/fatedier/frp/client/event"
|
|
"github.com/fatedier/frp/client/health"
|
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
"github.com/fatedier/frp/pkg/msg"
|
|
"github.com/fatedier/frp/pkg/naming"
|
|
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
|
"github.com/fatedier/frp/pkg/transport"
|
|
"github.com/fatedier/frp/pkg/util/xlog"
|
|
"github.com/fatedier/frp/pkg/vnet"
|
|
)
|
|
|
|
const (
|
|
ProxyPhaseNew = "new"
|
|
ProxyPhaseWaitStart = "wait start"
|
|
ProxyPhaseStartErr = "start error"
|
|
ProxyPhaseRunning = "running"
|
|
ProxyPhaseCheckFailed = "check failed"
|
|
ProxyPhaseClosed = "closed"
|
|
)
|
|
|
|
var (
|
|
statusCheckInterval = 3 * time.Second
|
|
waitResponseTimeout = 20 * time.Second
|
|
startErrTimeout = 30 * time.Second
|
|
)
|
|
|
|
type WorkingStatus struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Phase string `json:"status"`
|
|
Err string `json:"err"`
|
|
Cfg v1.ProxyConfigurer `json:"cfg"`
|
|
|
|
// Got from server.
|
|
RemoteAddr string `json:"remote_addr"`
|
|
}
|
|
|
|
type Wrapper struct {
|
|
WorkingStatus
|
|
|
|
// underlying proxy
|
|
pxy Proxy
|
|
|
|
// if ProxyConf has healcheck config
|
|
// monitor will watch if it is alive
|
|
monitor *health.Monitor
|
|
|
|
// event handler
|
|
handler event.Handler
|
|
|
|
msgTransporter transport.MessageTransporter
|
|
// vnet controller
|
|
vnetController *vnet.Controller
|
|
|
|
health uint32
|
|
lastSendStartMsg time.Time
|
|
lastStartErr time.Time
|
|
closeCh chan struct{}
|
|
healthNotifyCh chan struct{}
|
|
mu sync.RWMutex
|
|
|
|
xl *xlog.Logger
|
|
ctx context.Context
|
|
|
|
wireName string
|
|
}
|
|
|
|
func NewWrapper(
|
|
ctx context.Context,
|
|
cfg v1.ProxyConfigurer,
|
|
clientCfg *v1.ClientCommonConfig,
|
|
encryptionKey []byte,
|
|
eventHandler event.Handler,
|
|
msgTransporter transport.MessageTransporter,
|
|
vnetController *vnet.Controller,
|
|
udpPacketCodec string,
|
|
) *Wrapper {
|
|
baseInfo := cfg.GetBaseConfig()
|
|
xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name)
|
|
pw := &Wrapper{
|
|
WorkingStatus: WorkingStatus{
|
|
Name: baseInfo.Name,
|
|
Type: baseInfo.Type,
|
|
Phase: ProxyPhaseNew,
|
|
Cfg: cfg,
|
|
},
|
|
closeCh: make(chan struct{}),
|
|
healthNotifyCh: make(chan struct{}),
|
|
handler: eventHandler,
|
|
msgTransporter: msgTransporter,
|
|
vnetController: vnetController,
|
|
xl: xl,
|
|
ctx: xlog.NewContext(ctx, xl),
|
|
wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
|
}
|
|
|
|
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
|
pw.health = 1 // means failed
|
|
addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort))
|
|
pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr,
|
|
pw.statusNormalCallback, pw.statusFailedCallback)
|
|
xl.Tracef("enable health check monitor")
|
|
}
|
|
|
|
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController, udpPacketCodec)
|
|
return pw
|
|
}
|
|
|
|
func (pw *Wrapper) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
|
pw.pxy.SetInWorkConnCallback(cb)
|
|
}
|
|
|
|
func (pw *Wrapper) SetRunningStatus(remoteAddr string, respErr string) error {
|
|
pw.mu.Lock()
|
|
defer pw.mu.Unlock()
|
|
if pw.Phase != ProxyPhaseWaitStart {
|
|
return fmt.Errorf("status not wait start, ignore start message")
|
|
}
|
|
|
|
pw.RemoteAddr = remoteAddr
|
|
if respErr != "" {
|
|
pw.Phase = ProxyPhaseStartErr
|
|
pw.Err = respErr
|
|
pw.lastStartErr = time.Now()
|
|
return fmt.Errorf("%s", pw.Err)
|
|
}
|
|
|
|
if err := pw.pxy.Run(); err != nil {
|
|
pw.close()
|
|
pw.Phase = ProxyPhaseStartErr
|
|
pw.Err = err.Error()
|
|
pw.lastStartErr = time.Now()
|
|
return err
|
|
}
|
|
|
|
pw.Phase = ProxyPhaseRunning
|
|
pw.Err = ""
|
|
return nil
|
|
}
|
|
|
|
func (pw *Wrapper) Start() {
|
|
go pw.checkWorker()
|
|
if pw.monitor != nil {
|
|
go pw.monitor.Start()
|
|
}
|
|
}
|
|
|
|
func (pw *Wrapper) Stop() {
|
|
pw.mu.Lock()
|
|
defer pw.mu.Unlock()
|
|
close(pw.closeCh)
|
|
close(pw.healthNotifyCh)
|
|
pw.pxy.Close()
|
|
if pw.monitor != nil {
|
|
pw.monitor.Stop()
|
|
}
|
|
pw.Phase = ProxyPhaseClosed
|
|
pw.close()
|
|
}
|
|
|
|
func (pw *Wrapper) close() {
|
|
_ = pw.handler(&event.CloseProxyPayload{
|
|
CloseProxyMsg: &msg.CloseProxy{
|
|
ProxyName: pw.wireName,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (pw *Wrapper) checkWorker() {
|
|
xl := pw.xl
|
|
if pw.monitor != nil {
|
|
// let monitor do check request first
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
for {
|
|
// check proxy status
|
|
now := time.Now()
|
|
if atomic.LoadUint32(&pw.health) == 0 {
|
|
pw.mu.Lock()
|
|
if pw.Phase == ProxyPhaseNew ||
|
|
pw.Phase == ProxyPhaseCheckFailed ||
|
|
(pw.Phase == ProxyPhaseWaitStart && now.After(pw.lastSendStartMsg.Add(waitResponseTimeout))) ||
|
|
(pw.Phase == ProxyPhaseStartErr && now.After(pw.lastStartErr.Add(startErrTimeout))) {
|
|
|
|
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseWaitStart)
|
|
pw.Phase = ProxyPhaseWaitStart
|
|
|
|
var newProxyMsg msg.NewProxy
|
|
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
|
newProxyMsg.ProxyName = pw.wireName
|
|
pw.lastSendStartMsg = now
|
|
_ = pw.handler(&event.StartProxyPayload{
|
|
NewProxyMsg: &newProxyMsg,
|
|
})
|
|
}
|
|
pw.mu.Unlock()
|
|
} else {
|
|
pw.mu.Lock()
|
|
if pw.Phase == ProxyPhaseRunning || pw.Phase == ProxyPhaseWaitStart {
|
|
pw.close()
|
|
xl.Tracef("change status from [%s] to [%s]", pw.Phase, ProxyPhaseCheckFailed)
|
|
pw.Phase = ProxyPhaseCheckFailed
|
|
}
|
|
pw.mu.Unlock()
|
|
}
|
|
|
|
select {
|
|
case <-pw.closeCh:
|
|
return
|
|
case <-time.After(statusCheckInterval):
|
|
case <-pw.healthNotifyCh:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (pw *Wrapper) statusNormalCallback() {
|
|
xl := pw.xl
|
|
atomic.StoreUint32(&pw.health, 0)
|
|
_ = errors.PanicToError(func() {
|
|
select {
|
|
case pw.healthNotifyCh <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
xl.Infof("health check success")
|
|
}
|
|
|
|
func (pw *Wrapper) statusFailedCallback() {
|
|
xl := pw.xl
|
|
atomic.StoreUint32(&pw.health, 1)
|
|
_ = errors.PanicToError(func() {
|
|
select {
|
|
case pw.healthNotifyCh <- struct{}{}:
|
|
default:
|
|
}
|
|
})
|
|
xl.Infof("health check failed")
|
|
}
|
|
|
|
func (pw *Wrapper) InWorkConn(workConn net.Conn, m *msg.StartWorkConn) {
|
|
xl := pw.xl
|
|
pw.mu.RLock()
|
|
pxy := pw.pxy
|
|
running := pw.Phase == ProxyPhaseRunning
|
|
pw.mu.RUnlock()
|
|
if pxy != nil && running {
|
|
xl.Debugf("start a new work connection, localAddr: %s remoteAddr: %s", workConn.LocalAddr().String(), workConn.RemoteAddr().String())
|
|
go pxy.InWorkConn(workConn, m)
|
|
} else {
|
|
workConn.Close()
|
|
}
|
|
}
|
|
|
|
// reloadablePlugin owns its reload serialization and error snapshot. ReloadError
|
|
// must not block on I/O. Reload must be safe during Close, and retain usable
|
|
// resources on failure. Existing plugins need not implement this capability.
|
|
type reloadablePlugin interface {
|
|
plugin.Plugin
|
|
Reload() error
|
|
ReloadError() error
|
|
}
|
|
|
|
// Caller holds pw.mu. Concrete reloadable plugins are pointer instances.
|
|
func (pw *Wrapper) reloadablePluginLocked() reloadablePlugin {
|
|
owner, ok := pw.pxy.(interface{ getPlugin() plugin.Plugin })
|
|
if !ok {
|
|
return nil
|
|
}
|
|
p, _ := owner.getPlugin().(reloadablePlugin)
|
|
return p
|
|
}
|
|
|
|
func (pw *Wrapper) GetStatus() *WorkingStatus {
|
|
pw.mu.RLock()
|
|
defer pw.mu.RUnlock()
|
|
ps := &WorkingStatus{
|
|
Name: pw.Name,
|
|
Type: pw.Type,
|
|
Phase: pw.Phase,
|
|
Err: pw.Err,
|
|
Cfg: pw.Cfg,
|
|
RemoteAddr: pw.RemoteAddr,
|
|
}
|
|
if pw.Phase == ProxyPhaseRunning {
|
|
if p := pw.reloadablePluginLocked(); p != nil {
|
|
if err := p.ReloadError(); err != nil {
|
|
if ps.Err != "" {
|
|
ps.Err += "; "
|
|
}
|
|
ps.Err += fmt.Sprintf("reload plugin %s: %v", p.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
return ps
|
|
}
|