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
- Treat this as io.EOF-style termination: stop reading and exit the conn loop
- Check errors like this in the read loop and return without logging as fatal
- Avoid calling Read after the serve() exchange has returned; ensure single reader per conn
- 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
- Use a single reader goroutine per dnsServerConn
- Treat close as terminal: never Read after Close or after the reply is written
- Map this error to io.EOF in wrappers so downstream code handles it idiomatically
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
- broken pipe
- deadline not supported
- connection queue is full
- failed to perform an HTTPS request: %s
- empty chain
AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02).
Data as JSON: /api/errors/dc896acb513f37c5.
Report an issue: GitHub.