go-proxyproto icon indicating copy to clipboard operation
go-proxyproto copied to clipboard

Suggestion: Decouple Header Reading From net.Conn

Open adrianosela opened this issue 7 months ago • 0 comments

Having a sync.Once be checked in each net.Conn operation (i.e. on every Read()) seems quite inefficient. I suggest enqueuing accepted connections from the inner net.Listener's Accept(), processing/reading the header asynchronously, and having the publicly exposed net.Listener return the already-processed net.Conns...

Here's some pseudocode to illustrate what I mean:

import "net"

type listener struct {
	inner net.Listener

	in  chan net.Conn
	out chan net.Conn
}

// function that reads the proxy protocol header and returns a net.Conn
// implementation which returns the right address on RemoteAddr()
func processFunc(in net.Conn) net.Conn {
    // assume implemented
}

func NewListener(inner net.Listener) net.Listener {
	wrapped := &listener{
		inner: inner,
		in:    make(chan net.Conn, 1000),
		out:   make(chan net.Conn, 1000),
	}

	// kick-off go routine to process newly accepted connections
	go func() {
		for conn := range wrapped.in {
			conn := conn // avoid address capture
			go func() {
				processed := processFunc(conn)
				if processed != nil {
					wrapped.out <- processed
				}
			}()
		}
	}()

	// kick-off go routine to accept new connections
	go func() {
		for {
			conn, err := inner.Accept()
			if err != nil {
				return // accept errors are not recoverable
			}
			wrapped.in <- conn
		}
	}()

	return wrapped
}

func (l *listener) Accept() (net.Conn, error) {
	return <-l.out, nil
}

func (l *listener) Close() error {
	return l.inner.Close()
}

func (l *listener) Addr() net.Addr {
	return l.inner.Addr()
}

If there's interest in this I would love to submit a PR... seems indeed more efficient!

adrianosela avatar Jul 06 '24 22:07 adrianosela