Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix ports not being released on Service.Close() #4666

Merged
merged 1 commit into from
Feb 12, 2025

Conversation

ubergeek77
Copy link
Contributor

Description

Service.Close() leaves some listeners open after the frps server is closed, locking the ports until the entire program process is stopped. For CLI users of frps, this isn't an issue because the entire process stops when the server stops. But when using frps like a library, opening a server and then closing it will still leave some ports open. This prevents a new frps server from being opened if one was previously opened and then closed.

This PR fixes this by tracking all listeners frps opens, then closes them in the Close() function along with the other listeners.

Examples

The following code example will start an frps server, stop it, then attempt to start it again.

Code example
package main

import (
	"context"
	"time"

	_ "github.com/fatedier/frp/assets/frpc"
	_ "github.com/fatedier/frp/assets/frps"
	v1 "github.com/fatedier/frp/pkg/config/v1"
	frplog "github.com/fatedier/frp/pkg/util/log"
	"github.com/fatedier/frp/server"
)

// Store an instance of the FRP server globally
var FRPS *server.Service

// Run an Frp server
func startFrpServer() error {
	var defaultCfg v1.ServerConfig
	defaultCfg.Complete()
	svrCfg := &defaultCfg

	// Enable the webserver for demonstration
	svrCfg.WebServer.Addr = "0.0.0.0"
	svrCfg.WebServer.Port = 7500

	// Initialize frp's internal logger
	frplog.InitLogger(svrCfg.Log.To, svrCfg.Log.Level, int(svrCfg.Log.MaxDays), svrCfg.Log.DisablePrintColor)

	// Define a new FRPS server
	svr, err := server.NewService(svrCfg)
	if err != nil {
		panic(err)
	}

	// Start FRPS
	ctx := context.Background()
	go svr.Run(ctx)

	// Store it globally and return
	FRPS = svr
	return nil
}

// Close the frp server
func closeFrpServer() error {
	if err := FRPS.Close(); err != nil {
		return err
	}
	return nil
}

func main() {
	startFrpServer()
	time.Sleep(5 * time.Second)
	closeFrpServer()
	time.Sleep(5 * time.Second)
	startFrpServer()
	time.Sleep(5 * time.Second)
}

Without this PR, this will fail because the webserver port, 7500, is still in use:

2025-02-11 17:20:16.255 [I] [server/service.go:237] frps tcp listen on 0.0.0.0:7000
2025-02-11 17:20:16.256 [I] [server/service.go:351] dashboard listen on 0.0.0.0:7500
2025-02-11 17:20:21.257 [W] [server/service.go:485] Listener for incoming connections from client closed
panic: listen tcp 0.0.0.0:7500: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.

If I disable the webserver in the code example and run it again, the server still fails to start a second time, because port 7000 is still in use:

2025-02-11 17:28:37.204 [I] [server/service.go:237] frps tcp listen on 0.0.0.0:7000
2025-02-11 17:28:42.206 [W] [server/service.go:485] Listener for incoming connections from client closed
panic: create server listener error, listen tcp 0.0.0.0:7000: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.

With this PR, the previous server exits cleanly, and a new server can be started with no issue:

2025-02-11 17:22:22.724 [I] [server/service.go:246] frps tcp listen on 0.0.0.0:7000
2025-02-11 17:22:22.724 [I] [server/service.go:361] dashboard listen on 0.0.0.0:7500
2025-02-11 17:22:27.726 [W] [server/service.go:511] Listener for incoming connections from client closed
2025-02-11 17:22:27.726 [W] [server/service.go:363] dashboard server exit with error: http: Server closed
2025-02-11 17:22:32.759 [I] [server/service.go:246] frps tcp listen on 0.0.0.0:7000
2025-02-11 17:22:32.759 [I] [server/service.go:361] dashboard listen on 0.0.0.0:7500

Notes

I renamed listener to muxListener to disambiguate it from the other types of listeners. It ends up being defined by svr.muxer.DefaultListener(), so I named it that. This doesn't seem to be a breaking change because no parts of the code try to call the original svr.listener directly. But it doesn't matter what any of these are named as long as it gets closed properly.

@fatedier fatedier requested a review from Copilot February 12, 2025 03:12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 1 out of 1 changed files in this pull request and generated no comments.

Comments suppressed due to low confidence (2)

server/service.go:80

  • [nitpick] The renaming of 'listener' to 'muxListener' might cause confusion. Consider adding a comment to clarify the purpose of this change.
muxListener net.Listener

server/service.go:414

  • The error message 'Listener closed' could be more descriptive. Consider specifying which listener was closed.
svr.muxListener.Close()
@fatedier fatedier merged commit 8b86e14 into fatedier:dev Feb 12, 2025
2 checks passed
github-actions bot added a commit to aiastia-dockerhub/frp that referenced this pull request Feb 12, 2025
* https://github.com/fatedier/frp:
  update Release.md (fatedier#4668)
  frps: release resources in service.Close() (fatedier#4667)
  Fix ports not being released on Service.Close() (fatedier#4666)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants