Skip to content

proposal: x/net/http2: support padding at a higher, user-accessible layer #26900

Closed
@sergeyfrolov

Description

@sergeyfrolov

Abstract

This proposal describes a change to allow http/2 padding to be configured with Golang http/2 client and servers. The change is minimalistic, only changes the http2 package, and provides flexibility of the padding scheme.

Background

HTTP/2 spec says that “Padding can be used to obscure the exact size of frame content and is provided to mitigate specific attacks within HTTP, for example, attacks where compressed content includes both attacker-controlled plaintext and secret data (e.g., [BREACH]).” Padding may help against traffic analysis attack that detect which webpage on a certain website was visited. In addition to those use-cases, http/2 padding would be valuable when proxying: proxies may provide privacy protections against network adversaries that eavesdrop on connections between client and intermediary, but size of proxied packets may give out actual website/page/etc visited. Proxying of protocols with fixed-size payload, such as Tor cells, makes the proxied protocol immediately obvious on the wire, since the majority of packets will be of size TOR_CELL_SIZE*N + CONST_OVERHEAD.

However, specification warns that “Incorrectly implemented padding schemes can be easily defeated.”, and that even correctly implemented padding schemes may not provide as much protection, as it may seem. Spec also points out that “Correct application can depend on having specific knowledge of the data that is being padded”, underlying the importance of leaving specific application developers in charge of the padding scheme used.

Currently, http2 package exposes only WriteDataPadded method of low-level struct Framer, which in practice is not accessible from the http client or server structs. In order to use it, the developer would have to reimplement server and/or client, which is an untrivial and error-prone venture. Without changes to the standard library, Golang developers will not be able to use HTTP/2 padding and reap its benefits. For that reason we would like to add HTTP/2 padding support to the standard Golang http2 Server and ClientConn.

Proposal

All the proposed changes go into http2, which isn’t a part of stdlib, and, apparently, has a lower bar for knobs.
Padding scheme both on the client and server side will be set on the per-connection basis. Proposed func paddingScheme(int) uint8, that will be set by the application developer, takes the size of the data in the frame, and returns how much padding should be used. uint8 effectively enforces http2 padding limit of 255 bytes.

Client

http2.ClientConn struct will be expanded with paddingScheme func(int) uint8

Then the user would be able to set a preferred padding scheme using a new function:
func (cc *ClientConn) SetPaddingScheme(s func(int) uint8)

And in func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) we swap

err = cc.fr.WriteData(cs.ID, sentEnd, data)

(which just does WriteDataPadded(cs.ID, sentEnd, data, nil))

for

var padding []byte
if cc.paddingScheme != nil {
    padding = make([]byte, cc.paddingScheme(len(data)))
}
err = cc.fr.WriteDataPadded(cs.ID, sentEnd, data, padding)

Server

In order to enable http2 padding for http.Server, which doesn’t seem to expose desired knobs, we will add a SetPaddingScheme function to the http2.responseWriter. Then, the application developer should be able to wrap the http.HandleFunc with a check for the existence of SetPaddingScheme via interface cast, and set the scheme, when it’s an http2.responseWriter:

responseWriterState struct will be expanded with
paddingScheme func(int) uint8
and the http2.responseWriter, which holds responseWriterState will have a corresponding setter function:
func (w *responseWriter) SetPaddingScheme(scheme func(int) uint8)
Thus, func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) will be able to access its own paddingScheme

writeData struct will be expanded with byte []padding
writeData is created in the writeDataFromHandler function, which doesn’t have access to paddingScheme, so we will create padding buffer outside of this function, and change its signature from

func (sc *serverConn) writeDataFromHandler(stream *stream, data []byte, endStream bool) error

to

func (sc *serverConn) writeDataFromHandler(stream *stream, data []byte,  padding []byte, endStream bool) error

writeDataFromHandler is (only) called from func (rws *responseWriterState) writeChunk(p []byte) (n int, err error), which does have access to the paddingScheme so we can create the padding, and pass it into writeDataFromHandler.

This should be enough to add padding extension to Caddy Web Server, for instance.

Rationale

Padding scheme is configured on per-connection basis in order to provide better flexibility, and avoid adding extra knobs to high-level structs, such as http.Server. This approach does require additional code, but setting of the padding Scheme may be wrapped with the HandlerFunc, for all connections, if desired. This approach also allows developer to create a variable to count the frames, and close on it, allowing paddingScheme to depend on the data frame counter on per-connection basis, if desired.

PaddingScheme takes int for the size of the frame data, and returns uint8 to enforce the 255 byte padding limit. We can return int, but it is difficult to report error from ServeHTTP function, if padding size requested isn’t between 0 and 255. Alternatively, if requested padding size exceeds 255, we can generate new frames with padding and no data.

Currently, padding is dynamically allocated on each frame write, but only when paddingScheme is set. There are ways to optimize this for codepaths with padding, but they come with trade-offs. Global static array const zeroPadding [255]byte will avoid allocations, but will affect non-padding codepath. We can also have a global empty slice and sync.Once to allocate it once, but that also adds extra code and some extra overhead to non-padding codepath.

Compatibility

The change does not violate expectations of compatibility guidelines. It only adds new exported methods and unexported fields to exported structs.
Performance of the existing programs, that do not use padding, should not be affected substantially: there’s an additional check without lock acquisition for whether paddingScheme is set on every frame sent, but correct branch should be predicted consistently.

Implementation

Change itself is already implemented. Corresponding tests haven’t been written yet, awaiting the acceptance of design, but should be done in following weeks and is expected to be finished before November code freeze.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions