ginuerzh/gost · info

listener has been closed

Error message

listener has been closed

What it means

tunListener.Close uses a `closed` channel as a once-guard: if the channel is already closed, it returns "listener has been closed" instead of nil. This deviates from the usual net.Listener idiom where Close is idempotent, so a double Close surfaces as an error.

Source

Thrown at tuntap.go:115

func (l *tunListener) Accept() (net.Conn, error) {
	select {
	case conn := <-l.conns:
		return conn, nil
	case <-l.closed:
	}

	return nil, errors.New("accept on closed listener")
}

func (l *tunListener) Addr() net.Addr {
	return l.addr
}

func (l *tunListener) Close() error {
	select {
	case <-l.closed:
		return errors.New("listener has been closed")
	default:
		close(l.closed)
	}
	return nil
}

type tunHandler struct {
	options *HandlerOptions
	routes  sync.Map
	chExit  chan struct{}
}

// TunHandler creates a handler for tun tunnel.
func TunHandler(opts ...HandlerOption) Handler {
	h := &tunHandler{
		options: &HandlerOptions{},
		chExit:  make(chan struct{}, 1),
	}

View on GitHub (pinned to a33fdbf4c9)

Solutions

  1. Call Close only once per listener lifecycle; rely on the closed channel to signal other goroutines
  2. Ignore this specific error in cleanup paths (treat as success, like net.ErrClosed handling)
  3. Wrap Close in a sync.Once in your own code to guarantee idempotency

Example fix

// before
defer ln.Close()
...
if err := ln.Close(); err != nil { log.Fatal(err) }
// after
var closeOnce sync.Once
closeFn := func() { closeOnce.Do(func() { ln.Close() }) }
defer closeFn()
...
Defensive patterns

Strategy: validation

Validate before calling

var closed atomic.Bool
// before calling Close:
if closed.CompareAndSwap(false, true) {
    if err := ln.Close(); err != nil { log.Printf("close: %v", err) }
}

Try / catch

if err := ln.Close(); err != nil {
    if strings.Contains(err.Error(), "has been closed") {
        return nil // idempotent close: treat as success
    }
    return err
}

Prevention

When it happens

Trigger: Calling Close() twice on a tunListener, e.g. both an explicit shutdown path and a defer ln.Close(); Close after an external component already closed the listener.

Common situations: defer + explicit close patterns; restart/reload logic that closes listeners in more than one place; tests calling t.Cleanup close plus manual close.

Related errors


AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02). Data as JSON: /api/errors/cccf5790f34ae792. Report an issue: GitHub.