ginuerzh/gost · info

closed

Error message

closed

What it means

tcpRemoteForwardListener.Accept returns 'closed' when the listener's closed channel has been fired, i.e. Accept was called after Close(). This is the standard net.Listener convention signaling the listener is shutting down and no more connections will be delivered on connChan.

Source

Thrown at forward.go:446

		select {
		case l.connChan <- conn:
		default:
			conn.Close()
			log.Logf("[rtcp] %s - %s: connection queue is full", conn.RemoteAddr(), conn.LocalAddr())
		}
	}
}

func (l *tcpRemoteForwardListener) Accept() (conn net.Conn, err error) {
	if l.ln != nil {
		return l.ln.Accept()
	}

	select {
	case conn = <-l.connChan:
	case <-l.closed:
		err = errors.New("closed")
	}

	return
}

func (l *tcpRemoteForwardListener) accept() (conn net.Conn, err error) {
	lastNode := l.chain.LastNode()
	if lastNode.Protocol == "forward" && lastNode.Transport == "ssh" {
		return l.chain.Dial(l.addr.String())
	}

	if l.isChainValid() {
		if lastNode.GetBool("mbind") {
			return l.muxAccept() // multiplexing support for binding.
		}

		cc, er := l.chain.Conn()
		if er != nil {

View on GitHub (pinned to a33fdbf4c9)

Solutions

  1. Stop the Accept loop when this error is returned (standard shutdown path)
  2. Synchronize shutdown: close listener only after Accept loop exits or accept the benign error
  3. Compare error message/type to 'closed' and return io.EOF/ErrListenerClosed semantics upward
  4. Avoid logging it as fatal during shutdown

Example fix

// before
for {
    conn, err := l.Accept()
    if err != nil {
        log.Fatal(err)
    }
    go handle(conn)
}
// after
for {
    conn, err := l.Accept()
    if err != nil {
        if err.Error() == "closed" {
            return // listener shut down; exit loop cleanly
        }
        log.Log(err)
        continue
    }
    go handle(conn)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check listener state before entering the accept loop
select {
case <-l.closed:
    return // already closed; skip accept loop entirely
default:
}

Type guard

func isListenerClosed(err error) bool {
    return err != nil && err.Error() == "closed"
}

Try / catch

conn, err := l.Accept()
if err != nil {
    if isListenerClosed(err) {
        return nil // graceful shutdown: stop accepting
    }
    // handle transient error with tempDelay backoff
    return err
}

Prevention

When it happens

Trigger: Calling Accept on a remote-forward TCP listener after Close() was invoked, or racing Close with Accept so the select falls into the <-l.closed branch.

Common situations: Graceful shutdown of a remote port-forwarding server (gost rtcp), Stop/reload of services, test teardown ordering issues.

Related errors


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