cloudflare / cloudflare/cloudflared
🐛 bidirectional stream pipe immediate closing may causes errors in specific scenarios
- Dominant language
- Go
- Stars
- 15.6k
- Forks
- 1.4k
- PR merge metrics
- No merged PRs in 30d
Description
**Describe the bug**
Cloudflared's current stream relay behavior is, when one side (origin or tunnel) closes, the other side closes immediately:
https://github.com/cloudflare/cloudflared/blob/be64362fdb2a2da481f8e0414f75de3db2ccdf32/stream/stream.go#L43-L51
It's generally okay with most tunnel scenarios, but with some specific network clients/implementations, it causes errors.
For example, Client A connects to Server B through the cloudflared tunnel using a TCP connection. Client A calls `CloseWrite` to make the connection read-only, and expecting to continue reading from Server B. However, the stream terminates immediately after calling `CloseWrite`, preventing the expected behavior.
**To Reproduce**
1. Use provided client.go and server.go code samples
2. Run server.go with a specified port using `os.Args[1]`
3. Run client.go with a greeting message as an argument
4. Expected output on local server: `read from remote: hello`
5. Actual output: `read from remote: EOF`
client.go
```go
func main() {
if len(os.Args) <= 1 {
fmt.Println("please enter some greeting message")
return
}
conn, err := net.Dial("tcp", "your-ip:port")
if err != nil {
fmt.Println("dial:", err.Error())
return
}
defer conn.Close()
if _, err := conn.Write([]byte(os.Args[1])); err != nil {
fmt.Println("write greeting message to remote:", err.Error())
return
}
if err := conn.(*net.TCPConn).CloseWrite(); err != nil {
fmt.Println("close write:", err.Error())
return
}
ret := make([]byte, 1024)
nr, err := conn.Read(ret)
if err != nil {
fmt.Println("read from remote:", err.Error())
if nr > 0 {
fmt.Println("read from remote:", string(ret[:nr]))
}
return
}
fmt.Println("read from remote:", string(ret[:nr]))
}
```
server.go
```go
func main() {
if len(os.Args) <= 1 {
fmt.Println("please set server port using os.Args[1]")
return
}
lis, err := net.Listen("tcp", ":"+os.Args[1])
if err != nil {
fmt.Println("listen:", err.Error())
return
}
fmt.Println("server listen on :" + os.Args[1])
for {
conn, err := lis.Accept()
if err != nil {
fmt.Println("accept:", err.Error())
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
for {
b := make([]byte, 1024)
nr, err := conn.Read(b)
if err != nil {
fmt.Println("read:", err.Error())
if nr > 0 {
fmt.Println("read err, bug got:", b[:nr])
}
return
}
fmt.Println("read:", string(b[:nr]))
time.Sleep(10 * time.Second)
if _, err := conn.Write(b[:nr]); err != nil {
fmt.Println("write to remote:", err.Error())
}
}
}
```
So I wonder if there's a more graceful way to handle closing situations in cloudflared stream relay to prevent errors in the described scenario.
Contributor guide
Assessment
This issue has not been assessed yet.