ginuerzh/gost · info

connection is closed

Error message

connection is closed

What it means

dnsServerConn.Read returns this when the connection's cclose channel is fired, meaning the DNS request/response exchange has completed or been aborted before a queued message could be read. It emulates the 'read on closed conn' behavior of a net.Conn over DNS.

Source

Thrown at dns.go:357

	laddr, raddr net.Addr
}

func newDNSServerConn(laddr, raddr net.Addr) *dnsServerConn {
	return &dnsServerConn{
		mq:     make(chan []byte, 1),
		mr:     make(chan []byte, 1),
		laddr:  laddr,
		raddr:  raddr,
		cclose: make(chan struct{}),
	}
}

func (c *dnsServerConn) Read(b []byte) (n int, err error) {
	select {
	case mb := <-c.mq:
		n = copy(b, mb)
	case <-c.cclose:
		err = errors.New("connection is closed")
	}
	return
}

func (c *dnsServerConn) Write(b []byte) (n int, err error) {
	select {
	case c.mr <- b:
		n = len(b)
	case <-c.cclose:
		err = errors.New("broken pipe")
	}

	return
}

func (c *dnsServerConn) Close() error {
	select {
	case <-c.cclose:

View on GitHub (pinned to a33fdbf4c9)

Solutions

  1. Treat this as io.EOF-style termination: stop reading and exit the conn loop
  2. Check errors like this in the read loop and return without logging as fatal
  3. Avoid calling Read after the serve() exchange has returned; ensure single reader per conn
  4. Add errors.Is/if string match handling to map it to io.ErrClosedPipe for cleaner code

Example fix

// before
for {
    n, err := conn.Read(buf)
    if err != nil {
        log.Fatal(err)
    }
    handle(buf[:n])
}
// after
for {
    n, err := conn.Read(buf)
    if err != nil {
        if err.Error() == "connection is closed" {
            err = io.EOF // expected end of DNS exchange
        }
        break
    }
    handle(buf[:n])
}
Defensive patterns

Strategy: type-guard

Validate before calling

// ensure exactly one reader and that serve() has not returned before reading
select {
case <-connClosed():
    return // conn already closed, do not Read
default:
}

Type guard

func isConnClosed(err error) bool {
    return err != nil && (errors.Is(err, io.EOF) || err.Error() == "connection is closed")
}

Try / catch

n, err := conn.Read(buf)
if err != nil {
    if isConnClosed(err) {
        return io.EOF // expected end of DNS virtual conn
    }
    return err
}

Prevention

When it happens

Trigger: Calling Read on a dnsServerConn after Close() was called (server finished writing the response or timed out the exchange), so the mq channel has no producer and cclose is closed.

Common situations: Handler goroutine still reading after the DNS exchange ended, client query timeout causing early Close, double-response scenarios in DNS tunneling.

Related errors


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