allow to disable application layer heartbeat to reduce traffic cost (#2758)

fix #2754
This commit is contained in:
fatedier 2022-01-13 14:26:07 +08:00 committed by GitHub
parent 4bfc89d988
commit 293003fcdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 228 additions and 146 deletions

View File

@ -311,16 +311,27 @@ func (ctl *Control) msgHandler() {
}() }()
defer ctl.msgHandlerShutdown.Done() defer ctl.msgHandlerShutdown.Done()
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second) var hbSendCh <-chan time.Time
defer hbSend.Stop() // TODO(fatedier): disable heartbeat if TCPMux is enabled.
hbCheck := time.NewTicker(time.Second) // Just keep it here to keep compatible with old version frps.
defer hbCheck.Stop() if ctl.clientCfg.HeartbeatInterval > 0 {
hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second)
defer hbSend.Stop()
hbSendCh = hbSend.C
}
var hbCheckCh <-chan time.Time
// Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature.
if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux {
hbCheck := time.NewTicker(time.Second)
defer hbCheck.Stop()
hbCheckCh = hbCheck.C
}
ctl.lastPong = time.Now() ctl.lastPong = time.Now()
for { for {
select { select {
case <-hbSend.C: case <-hbSendCh:
// send heartbeat to server // send heartbeat to server
xl.Debug("send heartbeat to server") xl.Debug("send heartbeat to server")
pingMsg := &msg.Ping{} pingMsg := &msg.Ping{}
@ -329,7 +340,7 @@ func (ctl *Control) msgHandler() {
return return
} }
ctl.sendCh <- pingMsg ctl.sendCh <- pingMsg
case <-hbCheck.C: case <-hbCheckCh:
if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second { if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
// let reader() stop // let reader() stop

View File

@ -249,7 +249,7 @@ func (svr *Service) login() (conn net.Conn, session *fmux.Session, err error) {
if svr.cfg.TCPMux { if svr.cfg.TCPMux {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 20 * time.Second fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
session, err = fmux.Client(conn, fmuxCfg) session, err = fmux.Client(conn, fmuxCfg)
if err != nil { if err != nil {

View File

@ -61,6 +61,9 @@ pool_count = 5
# if tcp stream multiplexing is used, default is true, it must be same with frps # if tcp stream multiplexing is used, default is true, it must be same with frps
tcp_mux = true tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
# your proxy name will be changed to {user}.{proxy} # your proxy name will be changed to {user}.{proxy}
user = your_name user = your_name
@ -89,7 +92,8 @@ tls_enable = true
# start = ssh,dns # start = ssh,dns
# heartbeat configure, it's not recommended to modify the default value # heartbeat configure, it's not recommended to modify the default value
# the default value of heartbeat_interval is 10 and heartbeat_timeout is 90 # The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value
# to disable it.
# heartbeat_interval = 30 # heartbeat_interval = 30
# heartbeat_timeout = 90 # heartbeat_timeout = 90

View File

@ -92,7 +92,7 @@ oidc_skip_expiry_check = false
oidc_skip_issuer_check = false oidc_skip_issuer_check = false
# heartbeat configure, it's not recommended to modify the default value # heartbeat configure, it's not recommended to modify the default value
# the default value of heartbeat_timeout is 90 # the default value of heartbeat_timeout is 90. Set negative value to disable it.
# heartbeat_timeout = 90 # heartbeat_timeout = 90
# user_conn_timeout configure, it's not recommended to modify the default value # user_conn_timeout configure, it's not recommended to modify the default value
@ -121,6 +121,9 @@ subdomain_host = frps.com
# if tcp stream multiplexing is used, default is true # if tcp stream multiplexing is used, default is true
tcp_mux = true tcp_mux = true
# specify keep alive interval for tcp mux.
# only valid if tcp_mux is true.
# tcp_mux_keepalive_interval = 60
# custom 404 page for HTTP requests # custom 404 page for HTTP requests
# custom_404_page = /path/to/404.html # custom_404_page = /path/to/404.html

View File

@ -86,6 +86,9 @@ type ClientCommonConf struct {
// the server must have TCP multiplexing enabled as well. By default, this // the server must have TCP multiplexing enabled as well. By default, this
// value is true. // value is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// User specifies a prefix for proxy names to distinguish them from other // User specifies a prefix for proxy names to distinguish them from other
// clients. If this value is not "", proxy names will automatically be // clients. If this value is not "", proxy names will automatically be
// changed to "{user}.{proxy_name}". By default, this value is "". // changed to "{user}.{proxy_name}". By default, this value is "".
@ -129,11 +132,11 @@ type ClientCommonConf struct {
DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"` DisableCustomTLSFirstByte bool `ini:"disable_custom_tls_first_byte" json:"disable_custom_tls_first_byte"`
// HeartBeatInterval specifies at what interval heartbeats are sent to the // HeartBeatInterval specifies at what interval heartbeats are sent to the
// server, in seconds. It is not recommended to change this value. By // server, in seconds. It is not recommended to change this value. By
// default, this value is 30. // default, this value is 30. Set negative value to disable it.
HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"` HeartbeatInterval int64 `ini:"heartbeat_interval" json:"heartbeat_interval"`
// HeartBeatTimeout specifies the maximum allowed heartbeat response delay // HeartBeatTimeout specifies the maximum allowed heartbeat response delay
// before the connection is terminated, in seconds. It is not recommended // before the connection is terminated, in seconds. It is not recommended
// to change this value. By default, this value is 90. // to change this value. By default, this value is 90. Set negative value to disable it.
HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
// Client meta info // Client meta info
Metas map[string]string `ini:"-" json:"metas"` Metas map[string]string `ini:"-" json:"metas"`
@ -147,36 +150,37 @@ type ClientCommonConf struct {
// GetDefaultClientConf returns a client configuration with default values. // GetDefaultClientConf returns a client configuration with default values.
func GetDefaultClientConf() ClientCommonConf { func GetDefaultClientConf() ClientCommonConf {
return ClientCommonConf{ return ClientCommonConf{
ClientConfig: auth.GetDefaultClientConf(), ClientConfig: auth.GetDefaultClientConf(),
ServerAddr: "0.0.0.0", ServerAddr: "0.0.0.0",
ServerPort: 7000, ServerPort: 7000,
HTTPProxy: os.Getenv("http_proxy"), HTTPProxy: os.Getenv("http_proxy"),
LogFile: "console", LogFile: "console",
LogWay: "console", LogWay: "console",
LogLevel: "info", LogLevel: "info",
LogMaxDays: 3, LogMaxDays: 3,
DisableLogColor: false, DisableLogColor: false,
AdminAddr: "127.0.0.1", AdminAddr: "127.0.0.1",
AdminPort: 0, AdminPort: 0,
AdminUser: "", AdminUser: "",
AdminPwd: "", AdminPwd: "",
AssetsDir: "", AssetsDir: "",
PoolCount: 1, PoolCount: 1,
TCPMux: true, TCPMux: true,
User: "", TCPMuxKeepaliveInterval: 60,
DNSServer: "", User: "",
LoginFailExit: true, DNSServer: "",
Start: make([]string, 0), LoginFailExit: true,
Protocol: "tcp", Start: make([]string, 0),
TLSEnable: false, Protocol: "tcp",
TLSCertFile: "", TLSEnable: false,
TLSKeyFile: "", TLSCertFile: "",
TLSTrustedCaFile: "", TLSKeyFile: "",
HeartbeatInterval: 30, TLSTrustedCaFile: "",
HeartbeatTimeout: 90, HeartbeatInterval: 30,
Metas: make(map[string]string), HeartbeatTimeout: 90,
UDPPacketSize: 1500, Metas: make(map[string]string),
IncludeConfigFiles: make([]string, 0), UDPPacketSize: 1500,
IncludeConfigFiles: make([]string, 0),
} }
} }
@ -189,12 +193,10 @@ func (cfg *ClientCommonConf) Complete() {
} }
func (cfg *ClientCommonConf) Validate() error { func (cfg *ClientCommonConf) Validate() error {
if cfg.HeartbeatInterval <= 0 { if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 {
return fmt.Errorf("invalid heartbeat_interval") if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
} return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
}
if cfg.HeartbeatTimeout < cfg.HeartbeatInterval {
return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval")
} }
if cfg.TLSEnable == false { if cfg.TLSEnable == false {

View File

@ -259,33 +259,34 @@ func Test_LoadClientCommonConf(t *testing.T) {
OidcTokenEndpointURL: "endpoint_url", OidcTokenEndpointURL: "endpoint_url",
}, },
}, },
ServerAddr: "0.0.0.9", ServerAddr: "0.0.0.9",
ServerPort: 7009, ServerPort: 7009,
HTTPProxy: "http://user:passwd@192.168.1.128:8080", HTTPProxy: "http://user:passwd@192.168.1.128:8080",
LogFile: "./frpc.log9", LogFile: "./frpc.log9",
LogWay: "file", LogWay: "file",
LogLevel: "info9", LogLevel: "info9",
LogMaxDays: 39, LogMaxDays: 39,
DisableLogColor: false, DisableLogColor: false,
AdminAddr: "127.0.0.9", AdminAddr: "127.0.0.9",
AdminPort: 7409, AdminPort: 7409,
AdminUser: "admin9", AdminUser: "admin9",
AdminPwd: "admin9", AdminPwd: "admin9",
AssetsDir: "./static9", AssetsDir: "./static9",
PoolCount: 59, PoolCount: 59,
TCPMux: true, TCPMux: true,
User: "your_name", TCPMuxKeepaliveInterval: 60,
LoginFailExit: true, User: "your_name",
Protocol: "tcp", LoginFailExit: true,
TLSEnable: true, Protocol: "tcp",
TLSCertFile: "client.crt", TLSEnable: true,
TLSKeyFile: "client.key", TLSCertFile: "client.crt",
TLSTrustedCaFile: "ca.crt", TLSKeyFile: "client.key",
TLSServerName: "example.com", TLSTrustedCaFile: "ca.crt",
DNSServer: "8.8.8.9", TLSServerName: "example.com",
Start: []string{"ssh", "dns"}, DNSServer: "8.8.8.9",
HeartbeatInterval: 39, Start: []string{"ssh", "dns"},
HeartbeatTimeout: 99, HeartbeatInterval: 39,
HeartbeatTimeout: 99,
Metas: map[string]string{ Metas: map[string]string{
"var1": "123", "var1": "123",
"var2": "234", "var2": "234",

View File

@ -118,6 +118,9 @@ type ServerCommonConf struct {
// from a client to share a single TCP connection. By default, this value // from a client to share a single TCP connection. By default, this value
// is true. // is true.
TCPMux bool `ini:"tcp_mux" json:"tcp_mux"` TCPMux bool `ini:"tcp_mux" json:"tcp_mux"`
// TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler.
// If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux.
TCPMuxKeepaliveInterval int64 `ini:"tcp_mux_keepalive_interval" json:"tcp_mux_keepalive_interval"`
// Custom404Page specifies a path to a custom 404 page to display. If this // Custom404Page specifies a path to a custom 404 page to display. If this
// value is "", a default page will be displayed. By default, this value is // value is "", a default page will be displayed. By default, this value is
// "". // "".
@ -154,7 +157,7 @@ type ServerCommonConf struct {
TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"` TLSTrustedCaFile string `ini:"tls_trusted_ca_file" json:"tls_trusted_ca_file"`
// HeartBeatTimeout specifies the maximum time to wait for a heartbeat // HeartBeatTimeout specifies the maximum time to wait for a heartbeat
// before terminating the connection. It is not recommended to change this // before terminating the connection. It is not recommended to change this
// value. By default, this value is 90. // value. By default, this value is 90. Set negative value to disable it.
HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"` HeartbeatTimeout int64 `ini:"heartbeat_timeout" json:"heartbeat_timeout"`
// UserConnTimeout specifies the maximum time to wait for a work // UserConnTimeout specifies the maximum time to wait for a work
// connection. By default, this value is 10. // connection. By default, this value is 10.
@ -170,42 +173,43 @@ type ServerCommonConf struct {
// defaults. // defaults.
func GetDefaultServerConf() ServerCommonConf { func GetDefaultServerConf() ServerCommonConf {
return ServerCommonConf{ return ServerCommonConf{
ServerConfig: auth.GetDefaultServerConf(), ServerConfig: auth.GetDefaultServerConf(),
BindAddr: "0.0.0.0", BindAddr: "0.0.0.0",
BindPort: 7000, BindPort: 7000,
BindUDPPort: 0, BindUDPPort: 0,
KCPBindPort: 0, KCPBindPort: 0,
ProxyBindAddr: "", ProxyBindAddr: "",
VhostHTTPPort: 0, VhostHTTPPort: 0,
VhostHTTPSPort: 0, VhostHTTPSPort: 0,
TCPMuxHTTPConnectPort: 0, TCPMuxHTTPConnectPort: 0,
VhostHTTPTimeout: 60, VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0", DashboardAddr: "0.0.0.0",
DashboardPort: 0, DashboardPort: 0,
DashboardUser: "", DashboardUser: "",
DashboardPwd: "", DashboardPwd: "",
EnablePrometheus: false, EnablePrometheus: false,
AssetsDir: "", AssetsDir: "",
LogFile: "console", LogFile: "console",
LogWay: "console", LogWay: "console",
LogLevel: "info", LogLevel: "info",
LogMaxDays: 3, LogMaxDays: 3,
DisableLogColor: false, DisableLogColor: false,
DetailedErrorsToClient: true, DetailedErrorsToClient: true,
SubDomainHost: "", SubDomainHost: "",
TCPMux: true, TCPMux: true,
AllowPorts: make(map[int]struct{}), TCPMuxKeepaliveInterval: 60,
MaxPoolCount: 5, AllowPorts: make(map[int]struct{}),
MaxPortsPerClient: 0, MaxPoolCount: 5,
TLSOnly: false, MaxPortsPerClient: 0,
TLSCertFile: "", TLSOnly: false,
TLSKeyFile: "", TLSCertFile: "",
TLSTrustedCaFile: "", TLSKeyFile: "",
HeartbeatTimeout: 90, TLSTrustedCaFile: "",
UserConnTimeout: 10, HeartbeatTimeout: 90,
Custom404Page: "", UserConnTimeout: 10,
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), Custom404Page: "",
UDPPacketSize: 1500, HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
} }
} }

View File

@ -131,15 +131,16 @@ func Test_LoadServerCommonConf(t *testing.T) {
12: struct{}{}, 12: struct{}{},
99: struct{}{}, 99: struct{}{},
}, },
MaxPoolCount: 59, MaxPoolCount: 59,
MaxPortsPerClient: 9, MaxPortsPerClient: 9,
TLSOnly: true, TLSOnly: true,
TLSCertFile: "server.crt", TLSCertFile: "server.crt",
TLSKeyFile: "server.key", TLSKeyFile: "server.key",
TLSTrustedCaFile: "ca.crt", TLSTrustedCaFile: "ca.crt",
SubDomainHost: "frps.com", SubDomainHost: "frps.com",
TCPMux: true, TCPMux: true,
UDPPacketSize: 1509, TCPMuxKeepaliveInterval: 60,
UDPPacketSize: 1509,
HTTPPlugins: map[string]plugin.HTTPPluginOptions{ HTTPPlugins: map[string]plugin.HTTPPluginOptions{
"user-manager": { "user-manager": {
@ -174,27 +175,28 @@ func Test_LoadServerCommonConf(t *testing.T) {
AuthenticateNewWorkConns: false, AuthenticateNewWorkConns: false,
}, },
}, },
BindAddr: "0.0.0.9", BindAddr: "0.0.0.9",
BindPort: 7009, BindPort: 7009,
BindUDPPort: 7008, BindUDPPort: 7008,
ProxyBindAddr: "0.0.0.9", ProxyBindAddr: "0.0.0.9",
VhostHTTPTimeout: 60, VhostHTTPTimeout: 60,
DashboardAddr: "0.0.0.0", DashboardAddr: "0.0.0.0",
DashboardUser: "", DashboardUser: "",
DashboardPwd: "", DashboardPwd: "",
EnablePrometheus: false, EnablePrometheus: false,
LogFile: "console", LogFile: "console",
LogWay: "console", LogWay: "console",
LogLevel: "info", LogLevel: "info",
LogMaxDays: 3, LogMaxDays: 3,
DetailedErrorsToClient: true, DetailedErrorsToClient: true,
TCPMux: true, TCPMux: true,
AllowPorts: make(map[int]struct{}), TCPMuxKeepaliveInterval: 60,
MaxPoolCount: 5, AllowPorts: make(map[int]struct{}),
HeartbeatTimeout: 90, MaxPoolCount: 5,
UserConnTimeout: 10, HeartbeatTimeout: 90,
HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), UserConnTimeout: 10,
UDPPacketSize: 1500, HTTPPlugins: make(map[string]plugin.HTTPPluginOptions),
UDPPacketSize: 1500,
}, },
}, },
} }

View File

@ -400,12 +400,19 @@ func (ctl *Control) manager() {
defer ctl.allShutdown.Start() defer ctl.allShutdown.Start()
defer ctl.managerShutdown.Done() defer ctl.managerShutdown.Done()
heartbeat := time.NewTicker(time.Second) var heartbeatCh <-chan time.Time
defer heartbeat.Stop() if ctl.serverCfg.TCPMux || ctl.serverCfg.HeartbeatTimeout <= 0 {
// Don't need application heartbeat here.
// yamux will do same thing.
} else {
heartbeat := time.NewTicker(time.Second)
defer heartbeat.Stop()
heartbeatCh = heartbeat.C
}
for { for {
select { select {
case <-heartbeat.C: case <-heartbeatCh:
if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second { if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second {
xl.Warn("heartbeat timeout") xl.Warn("heartbeat timeout")
return return

View File

@ -406,7 +406,7 @@ func (svr *Service) HandleListener(l net.Listener) {
go func(ctx context.Context, frpConn net.Conn) { go func(ctx context.Context, frpConn net.Conn) {
if svr.cfg.TCPMux { if svr.cfg.TCPMux {
fmuxCfg := fmux.DefaultConfig() fmuxCfg := fmux.DefaultConfig()
fmuxCfg.KeepAliveInterval = 20 * time.Second fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second
fmuxCfg.LogOutput = io.Discard fmuxCfg.LogOutput = io.Discard
session, err := fmux.Server(frpConn, fmuxCfg) session, err := fmux.Server(frpConn, fmuxCfg)
if err != nil { if err != nil {

View File

@ -0,0 +1,48 @@
package features
import (
"fmt"
"time"
"github.com/fatedier/frp/test/e2e/framework"
. "github.com/onsi/ginkgo"
)
var _ = Describe("[Feature: Heartbeat]", func() {
f := framework.NewDefaultFramework()
It("disable application layer heartbeat", func() {
serverPort := f.AllocPort()
serverConf := fmt.Sprintf(`
[common]
bind_addr = 0.0.0.0
bind_port = %d
heartbeat_timeout = -1
tcp_mux_keepalive_interval = 2
`, serverPort)
remotePort := f.AllocPort()
clientConf := fmt.Sprintf(`
[common]
server_port = %d
log_level = trace
heartbeat_interval = -1
heartbeat_timeout = -1
tcp_mux_keepalive_interval = 2
[tcp]
type = tcp
local_port = %d
remote_port = %d
`, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)
// run frps and frpc
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
time.Sleep(5 * time.Second)
framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure()
})
})