-
Notifications
You must be signed in to change notification settings - Fork 17.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
net: no way to detect half-closed tcp connection #67337
Comments
Go can only expose the information that the kernel exposes. Does the Linux kernel have a way to distinguish a half-closed connection from a closed one? |
Yes, |
Please note that when your peer If the peer sends FIN, your |
In general, it is not possible to know whether the remote peer has closed or-half closed the connection. This is not because the kernel wouldn't expose such information, it is more fundamental. TCP doesn't have a signal for "full close". In TCP, a peer only signals that it has finished sending. The following is a network capture of a Linux 6.8.3 host (10.198.224.251) closing a connection. The closing packet sequence is identical whether a program calls TCPConn.Close (close), it calls TCPConn.CloseWrite (shutdown(SHUT_WR)), and even when the process is killed. A single FIN packet is sent by the host that closes the connection in all three cases.
So, even if the remote peer (10.198.224.251) has "full-closed" or even if the application has exited completely, the host 10.198.224.1 only knows that the remote peer has finished writing. The application running on 10.198.224.1 may still write to the socket without error (but if say the app has exited, this will trigger a RST and after receiving that, the next write will produce an error.) Also, note that calling TCPConn.CloseRead, does not trigger sending a single packet. So, I would say that this is not an issue with Go, but a feature of TCP. Suggested approach to fix your issue: Call your proxy P, and the peers A and B: A <-> P <-> B. When P reads EOF from A (connA), call connB.CloseWrite to half-close the connection with B. Continuing copying data from connB to connA. Upon reading EOF from connB, call connA.CloseWrite. For each connection, keep track of whether the other end has closed (EOF received) and whether you have closed (CloseWrite called). When both have closed, call Close. |
Here's an implementation of @antong's suggestion: func proxy(aConn, bConn net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(aConn, bConn) // Proxy data from B to A until EOF received
aConn.CloseWrite() // Proxy FIN from B to A
wg.Done()
}()
go func() {
io.Copy(bConn, aConn) // Proxy data from A to B until EOF received
bConn.CloseWrite() // Proxy FIN from A to B
wg.Done()
}()
wg.Wait()
aConn.Close()
bConn.Close()
} Unfortunately, it's prone to resource leaks. Let's say the connection to B is fully closed. The first goroutine will terminate, but the second goroutine will only terminate if it receives data from A (because it will try writing it to B and get an To fix this, you can use func proxy(aConn, bConn net.TCPConn) {
var wg sync.WaitGroup
wg.Add(4)
go func() {
io.Copy(aConn, bConn) // Proxy data from B to A until EOF received
aConn.CloseWrite() // Proxy FIN from B to A
wg.Done()
}()
go func() {
io.Copy(bConn, aConn) // Proxy data from A to B until EOF received
bConn.CloseWrite() // Proxy FIN from A to B
wg.Done()
}()
go func() {
waitForHup(bConn)
aConn.CloseRead() // Cause io.Copy(bConn, aConn) to return
wg.Done()
}()
go func() {
waitForHup(aConn)
bConn.CloseRead() // Cause io.Copy(aConn, bConn) to return
wg.Done()
}()
wg.Wait()
aConn.Close()
bConn.Close()
}
func waitForHup(conn net.Conn) error {
syscallConn, err := conn.(syscall.Conn).SyscallConn()
if err != nil {
return err
}
var pollErr error
if err := syscallConn.Control(func(fd uintptr) {
for {
pfds := []unix.PollFd{{
Fd: int32(fd),
}}
if _, err := unix.Poll(pfds, -1); err == nil {
return
} else if !errors.Is(err, unix.EINTR) {
pollErr = err
return
}
}
}); err != nil {
return err
}
return pollErr
} I've successfully used this technique to write proxies in C that handle full and half closes transparently. Unfortunately, in Go, calling |
Well for the example case to be a resource leak, A needs to neither read nor write forever. To handle such misbehaving peers, you need timeouts anyway. |
The problem is once As a proxy the use case is:
Currently there is no way to relay the Also I'm using tcp keepalive to detect dead connection, but I can't get the detection result once |
Timed out in state WaitingForInfo. Closing. (I am just a bot, though. Please speak up if this is a mistake or you have the requested information.) |
What else is needed? The use case is explained in #67337 (comment) and the possibility of implementation in #67337 (comment) |
Go version
go version go1.22.0 linux/amd64
Output of
go env
in your module/workspace:What did you do?
I'm trying to write a tcp proxy that accepting connection from client and proxy it to another server. The proxy should:
FIN
received from client to server and vice versa.RST
been received from either client or server.What did you see happen?
I can send
FIN
by callingconn.CloseWrite()
but I can't distinguish half-closed from closed tcp connection usingconn.Read
. All I got isio.EOF
, I need to know whether it's half-closed or not so I can make decision to callconn.CloseWrite()
orconn.Close()
.Reading the source code indicate that the information is lost in src/runtime/netpoll_epoll.go: netpoll which checked
syscall.EPOLLRDHUP
with a bunch of other flagsWhat did you expect to see?
Able to wait for half-close and close event for tcp connection
The text was updated successfully, but these errors were encountered: