protocol: don't buffer underlying conn reads after parsing header#148
protocol: don't buffer underlying conn reads after parsing header#148
Conversation
35b71de to
9321c93
Compare
| for len(readPayload) < payloadSize { | ||
| _ = conn.SetReadDeadline(time.Now().Add(time.Second)) | ||
| n, err := conn.Read(buf) | ||
| if err != nil && err != io.EOF { | ||
| t.Fatalf("unexpected read error: %v", err) | ||
| } | ||
| if n > 0 { | ||
| readPayload = append(readPayload, buf[:n]...) | ||
| } | ||
| if err == io.EOF { | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
this test is also passing with the previous MultiReader approach (although back then we always used the bufferedReader and never directly the conn, but that is also not tested).
If you want to stick with adhering strictly to the reader contract, I would modify this test so that we actually validate that we reach the underlying Conn (I can do that if you let me modify your branch). I.e. so that this test fails on the earlier version of the code. the modification amounts to doing a double-read with a 4KiB buffer and using a payload like 1KiB:
- we receive the buffered bytes
- we receive the remaining bytes with the following
Read()call.
There was a problem hiding this comment.
You should be able to open a PR against this PR. Since I'm working directly on this repo, I would have to give you write permissions to it so you could write to this branch, which is not ideal.
There was a problem hiding this comment.
IIUC the test fails with the earlier version of the code but passes now as documented in #148 (comment).
protocol.go
Outdated
| bufReader: br, | ||
| reader: io.MultiReader(br, conn), | ||
| conn: conn, | ||
| } |
There was a problem hiding this comment.
if you really don't want to slightly break the Reader Contract, then I would ask you to increase this bufSize to the default of 4 KiB (i.e. do a bufio.NewReader(conn)). this would mimic the old armon/go-proxyproto behavior, solve ingress nginx issue and potentially other users of this library.
in using this library though I'm expecting to transparently hit the underlying conn. therefore, when making a call to pConn.Read() with a 4KiB buffer, I am not at all expecting that the library will only return 210 bytes.
the alternatives are not compelling either: io.ReadAll would be reading too many bytes and io.ReadFull would return ErrUnexpectedEOF if there's an EOF before my buffer is filled.
if I want to mimic the "transparent" behavior (i.e. providing a 4KiB buffer and receiving 4KiB if those are available on the OS buffers), I'll have to make double Read calls as a user, to first drain the bufferedReader and then reach the actual Conn.
This workaround is both impractical and counter-intuitive, as I would expect from a library that transparently pipes an underlying conn to transparently give me access to the underlying conn, but at least with your fix I would actually reach the underlying Conn and this solution would work (with the previous version that would've been impossible)
There was a problem hiding this comment.
Please, see #150 for my preferred ways to deal with headers larger than 256 bytes.
Wrt to the Read(b) contract, the Go documentation is clear that it should return data available and not wait for b to be full. I will sleep over this and come back to you very soon.
There was a problem hiding this comment.
io.ReadFull would return ErrUnexpectedEOF if there's an EOF before my buffer is filled.
IIUC EOF would only be returned if the underlying net.Conn.Read(b) returns EOF, right?
That said, I think we're trying to address different challenges:
-
stop buffering the underlying conn after the the header is processed and the buffer is drained (fixed in this PR)
-
strictly follow
io.Readercontract or deliver a different undertanding, ieRead(b)will block untilbis full or EOF is returned -
increase the buffer size statically (eg Bump maximum header size to 4KiB, alternative version #155) or dinamically (protocol: safely resize header buffer based on length field #150)Bump maximum header size to 4KiB, alternative version #155 IS NOT about resizing the buffer BUT it does increase the v2 header size limit to 4KB. I have closed protocol: safely resize header buffer based on length field #150
This PR is meant to fix the 1 alone. Meanwhile, I enabled discussions for this repo so we can have a discussion wrt 2. cc @emersion
|
The test added by @clementnuss before the fix: After the fix (which also renames test from |
9321c93 to
d3a4ad1
Compare
|
Oh, actually, I think we need to add an extra check to the |
Sweet lord, nice catch! |
|
@emersion added tests for both read/write after Here's the write test before the fix: |
there's a severe bug in go-proxyproto, making it so that Conns wrapped by this library provide truncated data to the upstream. the core issue is that a MultiReader only concatenates it's Input Readers when those return an EOF. If an underlying input reader returns a number of bytes (because data was buffered, e.g. after a Peek()), then the MultiReader abstraction will not read data from the other InputReaders. this e.g. impacts ingress-nginx v1.14.x and breaks TLS passtrough in some scenarios, typically when the combined PROXY protocol header and TLS header are > 256 bytes. here's the core issue at play on the go playground: https://go.dev/play/p/wd2VVCvOPb_7
…onnReadHandlesChunkedPayload
590b137 to
1dabac2
Compare
|
Rebased to include #155. |
Fixes #120
Fixes #147
Refs #142
Please, critique @clementnuss @emersion.