fatedier/frp · warning

channel for kcp listener closed

Error message

channel for kcp listener closed

What it means

KCPListener.Accept receives from an acceptCh that the accept goroutine fills; when the channel is closed (by KCPListener.Close), the receive yields ok=false and this error is returned. It is the standard 'listener closed' sentinel for KCP mode, analogous to net.ErrClosed for TCP listeners: accept loops should stop on it.

Source

Thrown at pkg/util/net/kcp.go:70

				continue
			}
			conn.SetStreamMode(true)
			conn.SetWriteDelay(true)
			conn.SetNoDelay(1, 20, 2, 1)
			conn.SetMtu(1350)
			conn.SetWindowSize(1024, 1024)
			conn.SetACKNoDelay(false)

			l.acceptCh <- conn
		}
	}()
	return l, err
}

func (l *KCPListener) Accept() (net.Conn, error) {
	conn, ok := <-l.acceptCh
	if !ok {
		return conn, fmt.Errorf("channel for kcp listener closed")
	}
	return conn, nil
}

func (l *KCPListener) Close() error {
	if !l.closeFlag {
		l.closeFlag = true
		l.listener.Close()
	}
	return nil
}

func (l *KCPListener) Addr() net.Addr {
	return l.listener.Addr()
}

func NewKCPConnFromUDP(conn *net.UDPConn, connected bool, raddr string) (net.Conn, error) {
	udpAddr, err := net.ResolveUDPAddr("udp", raddr)

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Treat this error as a stop signal in accept loops: break/return, do not retry.
  2. Compare by message or wrap KCPListener Close/Accept with your own closed flag/errgroup cancellation if you need errors.Is semantics.
  3. Ensure Close() is called exactly once (guarded by closeFlag) before joining accept goroutines.

Example fix

// before
for {
    conn, err := l.Accept()
    if err != nil {
        log.Errorf("accept failed: %v", err) // noisy on shutdown
        continue
    }
    go handle(conn)
}

// after
for {
    conn, err := l.Accept()
    if err != nil {
        if strings.Contains(err.Error(), "listener closed") || strings.Contains(err.Error(), "channel for kcp listener closed") {
            return // shutdown
        }
        log.Errorf("accept failed: %v", err)
        continue
    }
    go handle(conn)
}
Defensive patterns

Strategy: try-catch

Type guard

func isListenerClosedErr(err error) bool {
    if errors.Is(err, net.ErrClosed) {
        return true
    }
    msg := err.Error()
    return strings.Contains(msg, "channel for kcp listener closed") ||
        strings.Contains(msg, "listener closed")
}

Try / catch

conn, err := kcpListener.Accept()
if err != nil {
    if isListenerClosedErr(err) {
        return // shutdown — stop the accept loop
    }
    log.Warnf("kcp accept error: %v", err)
    continue
}

Prevention

When it happens

Trigger: Calling Accept() after Close() on a KCP listener, or concurrently with Close() — the race resolves to either one final conn or this error. Happens during frps shutdown when transport.protocol = kcp.

Common situations: Graceful shutdown paths not checking for this error and logging it as unexpected; accept loops that retry on any error instead of terminating.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/1946ed6d0bb1e0d2. Report an issue: GitHub.