golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.65k stars 17.61k forks source link

proposal: net/tspsock: filter/interceptor - concept with default implementation for proxyprotocol (v1/v2) #65078

Closed HappyTobi closed 8 months ago

HappyTobi commented 9 months ago

Proposal Details

Abstract

Introduce a filter / interceptor that will be called in the TCPConn Read function, that can be configured on the TCPListener. The TCPConn should be the same as today, so no default filter should be apply.

Background

Whenever we are using an underlying TCP connection, the implementations like http break when a TCP Package contains a ProxyProtocol V1 or V2 header. The problem in the http component is how the TCP package is processed. The important part here is, that http should not have to care about the TCP Package itself, it should only care about the http package. To solve problems like that, we should give the users more flexibility on how a TCP Package is processed. To provide that flexibility, we should introduce a filter or interceptor to the TCPConn.

Proposal

The proposal is to add a filter or interceptor to the TCPListener that is used in the TCPConnection itself and allows that every package that is received can be "filtered". The filter must be called before the TCPConn is doing the syscall receiving the TCP package on which the filter or interceptor should be applied. The filter should be able to modify / change the TCP Package before it is returned to the caller.

The interface that is provided can also be used to implement something like a callback where for example the ProxyProtocol V1 or V2 header can be processed.

To have something that is working out of the box for an http server, for example, we should introduce an implementation that drops the ProxyProtocol header if a user is configuring the server with the filter.

Compatibility

The introduction of the filter / interceptor should not break any existing code. The new introduced filter / interceptor is optional. When a filter / interceptor is required, it can be configured on the TCPListener.

Implementation

The implementation / example is not finished but should show the idea behind the proposal.

//Example http server
mux := http.NewServeMux()
mux.HandleFunc("/", root)
ls := &net.ListenConfg{
    PackageFilter: ne.ProxyProtocolDropFilter,
}
server := &http.Serve{Addr: ":80", Handler: mux, ListenConfig: ls}
server.ListenAndServe()

//package net

// TCPFilter type definition
type TCPFilter func(co conn, s int) ([]byte, int, error)

//Add rhe Read(b []byte) (n int, err error) to the TCPConn
func (c *TCPConn) Read(b []byte) (int, error) {
    if c.filter != nil {
        r, l, err := c.filter(c.conn, len(b))
        if err != nil {
            return l, err
        }

        copy(b, r)
        return l, err
    }
    return c.conn.Read(b)
}

// Example filter implementation
var (
    PPV1 = []byte{'\x50', '\x52', '\x4F', '\x58', '\x59'}
    PPV2 = []byte{'\x0D', '\x0A', '\x0D', '\x0A', '\x00', '\x0D', '\x0A', '\x51', '\x55', '\x49', '\x54', '\x0A'}
)

// Filter implementation
func ProxyProtocolDropFilter(co conn, s int) ([]byte, int, error) {
    r := make([]byte, s)
    l, err := co.Read(r)
    if err != nil {
        return []byte{}, l, err
    }

    // Check if the package is smaller than 5 bytes, we can't check the header version.
    if s < 5 {
        return r, l, nil
    }

    if bytes.Equal(r[:5], PPV1) {
        i := bytes.Index(r[:108], []byte("\r\n"))
        return r[i+2:], l, nil
    }

    //Add more checks (just an example)
    if bytes.Equal(r[:13], PPV2) {
        fmt.Print("handle ppv2")
    }

    return r, l, err
}
seankhliao commented 9 months ago

A third party proxy protocol package could just wrap a net.Listener and expose itself as a net.Listener for net/http.Server.Serve. Which is what already existing packages do: https://pkg.go.dev/github.com/blacktear23/go-proxyprotocol#NewListener

Why should we add a more complex and less flexible filtering system into net?

HappyTobi commented 9 months ago

Hi,

I know that kind of implementations and know that writing a "custom" net.Listener can handle that.

The only problem that I see is, that "default" (for example http server) implementation will fail when you receive a tcp package that includes a proxy protocol. So my idea was to add something that is "issue" is handled by default / configuration.

ianlancetaylor commented 8 months ago

Thanks, but in the Go standard library filters are implemented by wrapping readers. If HTTP needs a way to filter its reader, then we should add a way to customize the reader that it uses. We should add something inside TCPConn. I'm going to close this proposal. Please comment if you disagree.