mirror of
https://github.com/fatedier/frp.git
synced 2026-07-25 18:29:18 +08:00
Compare commits
1 Commits
681fa87fae
...
feb98d3cda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb98d3cda |
@@ -1,8 +1,3 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
|
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
|
||||||
* Individual frpc proxies and visitors now accept an `enabled` flag (defaults to true), letting you disable specific entries without relying on the global `start` list—disabled blocks are skipped when client configs load.
|
|
||||||
|
|
||||||
## Improvements
|
|
||||||
|
|
||||||
* **VirtualNet**: Implemented intelligent reconnection with exponential backoff. When connection errors occur repeatedly, the reconnect interval increases from 60s to 300s (max), reducing unnecessary reconnection attempts. Normal disconnections still reconnect quickly at 10s intervals.
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -82,22 +81,11 @@ func (sv *STCPVisitor) internalConnWorker() {
|
|||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
var tunnelErr error
|
defer userConn.Close()
|
||||||
defer func() {
|
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userConn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
@@ -114,7 +102,6 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +110,12 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +125,6 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func NewVisitor(
|
|||||||
Name: cfg.GetBaseConfig().Name,
|
Name: cfg.GetBaseConfig().Name,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
VnetController: helper.VNetController(),
|
VnetController: helper.VNetController(),
|
||||||
SendConnToVisitor: func(conn net.Conn) {
|
HandleConn: func(conn net.Conn) {
|
||||||
_ = baseVisitor.AcceptConn(conn)
|
_ = baseVisitor.AcceptConn(conn)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -162,16 +162,8 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
|||||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
isConnTransferred := false
|
isConnTransferred := false
|
||||||
var tunnelErr error
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isConnTransferred {
|
if !isConnTransferred {
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userConn.Close()
|
userConn.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -189,8 +181,6 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
tunnelConn, err := sv.openTunnel(ctx)
|
tunnelConn, err := sv.openTunnel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("open tunnel error: %v", err)
|
xl.Errorf("open tunnel error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
|
|
||||||
// no fallback, just return
|
// no fallback, just return
|
||||||
if sv.cfg.FallbackTo == "" {
|
if sv.cfg.FallbackTo == "" {
|
||||||
return
|
return
|
||||||
@@ -210,7 +200,6 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,11 +143,6 @@ transport.tls.enable = true
|
|||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
# start = ["ssh", "dns"]
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
|
||||||
# Set 'enabled = false' in a proxy configuration to disable it.
|
|
||||||
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
|
||||||
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
|
||||||
|
|
||||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
# This parameter should be same between client and server.
|
# This parameter should be same between client and server.
|
||||||
# It affects the udp and sudp proxy.
|
# It affects the udp and sudp proxy.
|
||||||
@@ -174,8 +169,6 @@ metadatas.var2 = "123"
|
|||||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
name = "ssh"
|
name = "ssh"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
|
||||||
# enabled = true
|
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 22
|
localPort = 22
|
||||||
# Limit bandwidth for this proxy, unit is KB and MB
|
# Limit bandwidth for this proxy, unit is KB and MB
|
||||||
@@ -260,8 +253,6 @@ healthCheck.httpHeaders=[
|
|||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "web02"
|
name = "web02"
|
||||||
type = "https"
|
type = "https"
|
||||||
# Disable this proxy by setting enabled to false
|
|
||||||
# enabled = false
|
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 8000
|
localPort = 8000
|
||||||
subdomain = "web02"
|
subdomain = "web02"
|
||||||
|
|||||||
@@ -281,17 +281,6 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by enabled field in each proxy
|
|
||||||
// nil or true means enabled, false means disabled
|
|
||||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
|
||||||
enabled := c.GetBaseConfig().Enabled
|
|
||||||
return enabled == nil || *enabled
|
|
||||||
})
|
|
||||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
|
||||||
enabled := c.GetBaseConfig().Enabled
|
|
||||||
return enabled == nil || *enabled
|
|
||||||
})
|
|
||||||
|
|
||||||
if cliCfg != nil {
|
if cliCfg != nil {
|
||||||
if err := cliCfg.Complete(); err != nil {
|
if err := cliCfg.Complete(); err != nil {
|
||||||
return nil, nil, nil, isLegacyFormat, err
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
|||||||
@@ -110,9 +110,6 @@ type DomainConfig struct {
|
|||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
|
||||||
// This allows individual control over each proxy, complementing the global "start" field.
|
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
Transport ProxyTransport `json:"transport,omitempty"`
|
Transport ProxyTransport `json:"transport,omitempty"`
|
||||||
// metadata info for each proxy
|
// metadata info for each proxy
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ type VisitorTransport struct {
|
|||||||
type VisitorBaseConfig struct {
|
type VisitorBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
|
||||||
// This allows individual control over each visitor, complementing the global "start" field.
|
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
|
||||||
Transport VisitorTransport `json:"transport,omitempty"`
|
Transport VisitorTransport `json:"transport,omitempty"`
|
||||||
SecretKey string `json:"secretKey,omitempty"`
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
// if the server user is not set, it defaults to the current user
|
// if the server user is not set, it defaults to the current user
|
||||||
|
|||||||
@@ -23,20 +23,11 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PluginContext provides the necessary context and callbacks for visitor plugins.
|
|
||||||
type PluginContext struct {
|
type PluginContext struct {
|
||||||
// Name is the unique identifier for this visitor, used for logging and routing.
|
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
|
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
|
|
||||||
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
|
|
||||||
VnetController *vnet.Controller
|
VnetController *vnet.Controller
|
||||||
|
HandleConn func(net.Conn)
|
||||||
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
|
|
||||||
// Does not return error; failures are handled by closing the connection.
|
|
||||||
SendConnToVisitor func(net.Conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creators is used for create plugins to handle connections.
|
// Creators is used for create plugins to handle connections.
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ type VirtualNetPlugin struct {
|
|||||||
controllerConn net.Conn
|
controllerConn net.Conn
|
||||||
closeSignal chan struct{}
|
closeSignal chan struct{}
|
||||||
|
|
||||||
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
@@ -100,6 +98,7 @@ func (p *VirtualNetPlugin) Start() {
|
|||||||
|
|
||||||
func (p *VirtualNetPlugin) run() {
|
func (p *VirtualNetPlugin) run() {
|
||||||
xl := xlog.FromContextSafe(p.ctx)
|
xl := xlog.FromContextSafe(p.ctx)
|
||||||
|
reconnectDelay := 10 * time.Second
|
||||||
|
|
||||||
for {
|
for {
|
||||||
currentCloseSignal := make(chan struct{})
|
currentCloseSignal := make(chan struct{})
|
||||||
@@ -122,10 +121,7 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.controllerConn = controllerConn
|
p.controllerConn = controllerConn
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
// Wrap with CloseNotifyConn which supports both close notification and error recording
|
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
|
||||||
var closeErr error
|
|
||||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
|
|
||||||
closeErr = err
|
|
||||||
close(currentCloseSignal) // Signal the run loop on close.
|
close(currentCloseSignal) // Signal the run loop on close.
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,9 +129,9 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
||||||
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
||||||
|
|
||||||
// Pass the CloseNotifyConn to the visitor for handling.
|
// Pass the CloseNotifyConn to HandleConn.
|
||||||
// The visitor can call CloseWithError to record the failure reason.
|
// HandleConn is responsible for calling Close() on pluginNotifyConn.
|
||||||
p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
|
p.pluginCtx.HandleConn(pluginNotifyConn)
|
||||||
|
|
||||||
// Wait for context cancellation or connection close.
|
// Wait for context cancellation or connection close.
|
||||||
select {
|
select {
|
||||||
@@ -144,32 +140,8 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
return
|
return
|
||||||
case <-currentCloseSignal:
|
case <-currentCloseSignal:
|
||||||
// Determine reconnect delay based on error with exponential backoff
|
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
|
||||||
var reconnectDelay time.Duration
|
// HandleConn closed the plugin side. Close the controller side.
|
||||||
if closeErr != nil {
|
|
||||||
p.consecutiveErrors++
|
|
||||||
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
|
|
||||||
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
|
|
||||||
|
|
||||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
|
||||||
baseDelay := 60 * time.Second
|
|
||||||
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
|
||||||
if reconnectDelay > 300*time.Second {
|
|
||||||
reconnectDelay = 300 * time.Second
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset consecutive errors on successful connection
|
|
||||||
if p.consecutiveErrors > 0 {
|
|
||||||
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
|
|
||||||
p.pluginCtx.Name, p.consecutiveErrors)
|
|
||||||
p.consecutiveErrors = 0
|
|
||||||
} else {
|
|
||||||
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
|
|
||||||
}
|
|
||||||
reconnectDelay = 10 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// The visitor closed the plugin side. Close the controller side.
|
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
|
|
||||||
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
||||||
@@ -212,7 +184,7 @@ func (p *VirtualNetPlugin) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly close the controller side of the pipe.
|
// Explicitly close the controller side of the pipe.
|
||||||
// This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
|
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
||||||
|
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
|
|||||||
// 1 means closed
|
// 1 means closed
|
||||||
closeFlag int32
|
closeFlag int32
|
||||||
|
|
||||||
closeFn func(error)
|
closeFn func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
|
// closeFn will be only called once
|
||||||
func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||||
return &CloseNotifyConn{
|
return &CloseNotifyConn{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
closeFn: closeFn,
|
closeFn: closeFn,
|
||||||
@@ -151,25 +151,12 @@ func (cc *CloseNotifyConn) Close() (err error) {
|
|||||||
if pflag == 0 {
|
if pflag == 0 {
|
||||||
err = cc.Conn.Close()
|
err = cc.Conn.Close()
|
||||||
if cc.closeFn != nil {
|
if cc.closeFn != nil {
|
||||||
cc.closeFn(nil)
|
cc.closeFn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseWithError closes the connection and passes the error to the close callback.
|
|
||||||
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
|
||||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
|
||||||
if pflag == 0 {
|
|
||||||
closeErr := cc.Conn.Close()
|
|
||||||
if cc.closeFn != nil {
|
|
||||||
cc.closeFn(err)
|
|
||||||
}
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsConn struct {
|
type StatsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
|||||||
muxer := http.NewServeMux()
|
muxer := http.NewServeMux()
|
||||||
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
||||||
notifyCh := make(chan struct{})
|
notifyCh := make(chan struct{})
|
||||||
conn := WrapCloseNotifyConn(c, func(_ error) {
|
conn := WrapCloseNotifyConn(c, func() {
|
||||||
close(notifyCh)
|
close(notifyCh)
|
||||||
})
|
})
|
||||||
wl.acceptCh <- conn
|
wl.acceptCh <- conn
|
||||||
|
|||||||
Reference in New Issue
Block a user