traefik/pkg/server/router/tcp/postgres.go

167 lines
4.5 KiB
Go
Raw Normal View History

package tcp
import (
"bufio"
"bytes"
"errors"
"io"
"net"
"sync"
2022-11-21 17:36:05 +00:00
"github.com/rs/zerolog/log"
2023-02-03 14:24:05 +00:00
tcpmuxer "github.com/traefik/traefik/v3/pkg/muxer/tcp"
"github.com/traefik/traefik/v3/pkg/tcp"
)
var (
PostgresStartTLSMsg = []byte{0, 0, 0, 8, 4, 210, 22, 47} // int32(8) + int32(80877103)
PostgresStartTLSReply = []byte{83} // S
)
// isPostgres determines whether the buffer contains the Postgres STARTTLS message.
func isPostgres(br *bufio.Reader) (bool, error) {
// Peek the first 8 bytes individually to prevent blocking on peek
// if the underlying conn does not send enough bytes.
// It could happen if a protocol start by sending less than 8 bytes,
// and expect a response before proceeding.
for i := 1; i < len(PostgresStartTLSMsg)+1; i++ {
peeked, err := br.Peek(i)
if err != nil {
var opErr *net.OpError
2024-09-27 09:20:04 +00:00
if !errors.Is(err, io.EOF) && (!errors.As(err, &opErr) || !opErr.Timeout()) {
log.Error().Err(err).Msg("Error while Peeking first byte")
}
return false, err
}
if !bytes.Equal(peeked, PostgresStartTLSMsg[:i]) {
return false, nil
}
}
return true, nil
}
// servePostgres serves a connection with a Postgres client negotiating a STARTTLS session.
// It handles TCP TLS routing, after accepting to start the STARTTLS session.
func (r *Router) servePostgres(conn tcp.WriteCloser) {
_, err := conn.Write(PostgresStartTLSReply)
if err != nil {
conn.Close()
return
}
br := bufio.NewReader(conn)
b := make([]byte, len(PostgresStartTLSMsg))
_, err = br.Read(b)
if err != nil {
conn.Close()
return
}
hello, err := clientHelloInfo(br)
if err != nil {
conn.Close()
return
}
if !hello.isTLS {
conn.Close()
return
}
connData, err := tcpmuxer.NewConnData(hello.serverName, conn, hello.protos)
if err != nil {
2022-11-21 17:36:05 +00:00
log.Error().Err(err).Msg("Error while reading TCP connection data")
conn.Close()
return
}
// Contains also TCP TLS passthrough routes.
handlerTCPTLS, _ := r.muxerTCPTLS.Match(connData)
if handlerTCPTLS == nil {
conn.Close()
return
}
// We are in TLS mode and if the handler is not TLSHandler, we are in passthrough.
proxiedConn := r.GetConn(conn, hello.peeked)
if _, ok := handlerTCPTLS.(*tcp.TLSHandler); !ok {
proxiedConn = &postgresConn{WriteCloser: proxiedConn}
}
handlerTCPTLS.ServeTCP(proxiedConn)
}
// postgresConn is a tcp.WriteCloser that will negotiate a TLS session (STARTTLS),
// before exchanging any data.
// It enforces that the STARTTLS negotiation with the peer is successful.
type postgresConn struct {
tcp.WriteCloser
starttlsMsgSent bool // whether we have already sent the STARTTLS handshake to the backend.
starttlsReplyReceived bool // whether we have already received the STARTTLS handshake reply from the backend.
// errChan makes sure that an error is returned if the first operation to ever
// happen on a postgresConn is a Write (because it should instead be a Read).
errChanMu sync.Mutex
errChan chan error
}
// Read reads bytes from the underlying connection (tcp.WriteCloser).
// On first call, it actually only injects the PostgresStartTLSMsg,
// in order to behave as a Postgres TLS client that initiates a STARTTLS handshake.
// Read does not support concurrent calls.
func (c *postgresConn) Read(p []byte) (n int, err error) {
if c.starttlsMsgSent {
if err := <-c.errChan; err != nil {
return 0, err
}
return c.WriteCloser.Read(p)
}
defer func() {
c.starttlsMsgSent = true
c.errChanMu.Lock()
c.errChan = make(chan error)
c.errChanMu.Unlock()
}()
copy(p, PostgresStartTLSMsg)
return len(PostgresStartTLSMsg), nil
}
// Write writes bytes to the underlying connection (tcp.WriteCloser).
// On first call, it checks that the bytes to write (the ones provided by the backend)
// match the PostgresStartTLSReply, and if yes it drops them (as the STARTTLS
// handshake between the client and traefik has already taken place). Otherwise, an
// error is transmitted through c.errChan, so that the second Read call gets it and
// returns it up the stack.
// Write does not support concurrent calls.
func (c *postgresConn) Write(p []byte) (n int, err error) {
if c.starttlsReplyReceived {
return c.WriteCloser.Write(p)
}
c.errChanMu.Lock()
if c.errChan == nil {
c.errChanMu.Unlock()
return 0, errors.New("initial read never happened")
}
c.errChanMu.Unlock()
defer func() {
c.starttlsReplyReceived = true
}()
if len(p) != 1 || p[0] != PostgresStartTLSReply[0] {
c.errChan <- errors.New("invalid response from Postgres server")
return len(p), nil
}
close(c.errChan)
return 1, nil
}