Repository: fatedier/frp (language: go)

Issue:
failed after connect success on linux/arm64
### Bug Description

failed after connect success on linux/arm64

### frpc Version

0.53.0

### frps Version

0.53.0

### System Architecture

linux/arm64

### Configurations

```json
{
  "serverAddr": "frp.server",
  "serverPort": 7000,
    "auth": {
      "method": "token",
      "token": "token"
    },
    "proxies": [{
      "name": "jhit-deepin",
      "type": "tcp",
      "localIP": "127.0.0.1",
      "localPort": 22,
      "remotePort": 50022
    }]
}

```

### Logs

```
/etc/frp$ sudo ./frpc -c frpc.json

2023/12/21 19:02:10 [I] [root.go:141] start frpc service for config file [frpc.json]
2023/12/21 19:02:10 [I] [service.go:288] try to connect to server...
2023/12/21 19:02:15 [I] [service.go:279] [d70a69d35a2e3cf8] login to server success, get run id [d70a69d35a2e3cf8]
2023/12/21 19:02:15 [I] [proxy_manager.go:173] [d70a69d35a2e3cf8] proxy added: [jhit-deepin]
2023/12/21 19:02:15 [I] [service.go:288] [d70a69d35a2e3cf8] try to connect to server...
2023/12/21 19:02:16 [I] [control.go:169] [d70a69d35a2e3cf8] [jhit-deepin] start proxy success
2023/12/21 19:02:21 [I] [service.go:279] [d70a69d35a2e3cf8] login to server success, get run id [d70a69d35a2e3cf8]
2023/12/21 19:02:21 [I] [proxy_manager.go:173] [d70a69d35a2e3cf8] proxy added: [jhit-deepin]
2023/12/21 19:02:21 [I] [panic.go:884] frpc service for config file [frpc.json] stopped
panic: close of closed channel

goroutine 1 [running]:
github.com/fatedier/frp/client.(*Service).loopLoginUntilSuccess.func1()
        github.com/fatedier/frp/client/service.go:331 +0x498
github.com/fatedier/frp/pkg/util/wait.BackoffUntil(0x40001f99e0, {0x862b40, 0x40000e9710}, 0x1, 0x4000098420)
        github.com/fatedier/frp/pkg/util/wait/backoff.go:135 +0xe0
github.com/fatedier/frp/client.(*Service).loopLoginUntilSuccess(0x4000032460, 0x2540be400, 0x1)
        github.com/fatedier/frp/client/service.go:336 +0x164
github.com/fatedier/frp/client.(*Service).Run(0x4000032460, {0x868a10?, 0x40000340e0?})
        github.com/fatedier/frp/client/service.go:166 +0xf8
github.com/fatedier/frp/cmd/frpc/sub.startService(0x40001b8840, {0x4000189cf0, 0x1, 0x1}, {0xc99300, 0x0, 0x0}, {0xfffff4ffe47f, 0x9})
        github.com/fatedier/frp/cmd/frpc/sub/root.go:159 +0x2a0
github.com/fatedier/frp/cmd/frpc/sub.runClient({0xfffff4ffe47f, 0x9})
        github.com/fatedier/frp/cmd/frpc/sub/root.go:129 +0x138
github.com/fatedier/frp/cmd/frpc/sub.glob..func2(0xc57fc0?, {0x6be840?, 0x2?, 0x2?})
        github.com/fatedier/frp/cmd/frpc/sub/root.go:69 +0x4c
github.com/spf13/cobra.(*Command).execute(0xc57fc0, {0x4000030190, 0x2, 0x2})
        github.com/spf13/cobra@v1.8.0/command.go:983 +0x7b0
github.com/spf13/cobra.(*Command).ExecuteC(0xc57fc0)
        github.com/spf13/cobra@v1.8.0/command.go:1115 +0x364
github.com/spf13/cobra.(*Command).Execute(...)
        github.com/spf13/cobra@v1.8.0/command.go:1039
github.com/fatedier/frp/cmd/frpc/sub.Execute()
        github.com/fatedier/frp/cmd/frpc/sub/root.go:100 +0x24
main.main()
        github.com/fatedier/frp/cmd/frpc/main.go:23 +0x1c
```

### Steps to reproduce

1. 
2. 
3. 
...


### Affected area

- [ ] Docs
- [ ] Installation
- [ ] Performance and Scalability
- [ ] Security
- [ ] User Experience
- [ ] Test and Release
- [ ] Developer Infrastructure
- [ ] Client Plugin
- [ ] Server Plugin
- [ ] Extensions
- [ ] Others


Discussion / hints:

Fix patch:
diff --git a/client/control.go b/client/control.go
index e4b01ae8c21..edfc726c301 100644
--- a/client/control.go
+++ b/client/control.go
@@ -239,15 +239,15 @@ func (ctl *Control) heartbeatWorker() {
 	// Users can still enable heartbeat feature by setting HeartbeatInterval to a positive value.
 	if ctl.sessionCtx.Common.Transport.HeartbeatInterval > 0 {
 		// send heartbeat to server
-		sendHeartBeat := func() error {
+		sendHeartBeat := func() (bool, error) {
 			xl.Debug("send heartbeat to server")
 			pingMsg := &msg.Ping{}
 			if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
 				xl.Warn("error during ping authentication: %v, skip sending ping message", err)
-				return err
+				return false, err
 			}
 			_ = ctl.msgDispatcher.Send(pingMsg)
-			return nil
+			return false, nil
 		}
 
 		go wait.BackoffUntil(sendHeartBeat,
diff --git a/client/service.go b/client/service.go
index c43f8f60497..e8ecc2b5e99 100644
--- a/client/service.go
+++ b/client/service.go
@@ -192,16 +192,16 @@ func (svr *Service) keepControllerWorking() {
 	// 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() error {
+	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)
 		if svr.ctl != nil {
 			<-svr.ctl.Done()
-			return errors.New("control is closed and try another loop")
+			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 nil
+		return false, nil
 	}, wait.NewFastBackoffManager(
 		wait.FastBackoffOptions{
 			Duration:        time.Second,
@@ -282,9 +282,8 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
 
 func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginExit bool) {
 	xl := xlog.FromContextSafe(svr.ctx)
-	successCh := make(chan struct{})
 
-	loginFunc := func() error {
+	loginFunc := func() (bool, error) {
 		xl.Info("try to connect to server...")
 		conn, connector, err := svr.login()
 		if err != nil {
@@ -292,7 +291,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
 			if firstLoginExit {
 				svr.cancel(cancelErr{Err: err})
 			}
-			return err
+			return false, err
 		}
 
 		svr.cfgMu.RLock()
@@ -315,7 +314,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
 		if err != nil {
 			conn.Close()
 			xl.Error("NewControl error: %v", err)
-			return err
+			return false, err
 		}
 		ctl.SetInWorkConnCallback(svr.handleWorkConnCb)
 
@@ -328,8 +327,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
 		svr.ctl = ctl
 		svr.ctlMu.Unlock()
 
-		close(successCh)
-		return nil
+		return true, nil
 	}
 
 	// try to reconnect to server until success
@@ -339,9 +337,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
 			Factor:      2,
 			Jitter:      0.1,
 			MaxDuration: maxInterval,
-		}),
-		true,
-		wait.MergeAndCloseOnAnyStopChannel(svr.ctx.Done(), successCh))
+		}), true, svr.ctx.Done())
 }
 
 func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error {
diff --git a/pkg/util/wait/backoff.go b/pkg/util/wait/backoff.go
index 048c9591ade..4a0e7e0e4eb 100644
--- a/pkg/util/wait/backoff.go
+++ b/pkg/util/wait/backoff.go
@@ -113,7 +113,7 @@ func (f *fastBackoffImpl) Backoff(previousDuration time.Duration, previousCondit
 	return f.options.Duration
 }
 
-func BackoffUntil(f func() error, backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
+func BackoffUntil(f func() (bool, error), backoff BackoffManager, sliding bool, stopCh <-chan struct{}) {
 	var delay time.Duration
 	previousError := false
 
@@ -131,7 +131,9 @@ func BackoffUntil(f func() error, backoff BackoffManager, sliding bool, stopCh <
 			delay = backoff.Backoff(delay, previousError)
 		}
 
-		if err := f(); err != nil {
+		if done, err := f(); done {
+			return
+		} else if err != nil {
 			previousError = true
 		} else {
 			previousError = false
@@ -170,9 +172,9 @@ func Jitter(duration time.Duration, maxFactor float64) time.Duration {
 }
 
 func Until(f func(), period time.Duration, stopCh <-chan struct{}) {
-	ff := func() error {
+	ff := func() (bool, error) {
 		f()
-		return nil
+		return false, nil
 	}
 	BackoffUntil(ff, BackoffFunc(func(time.Duration, bool) time.Duration {
 		return period


Test patch: