Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cae9419cd5
|
|||
|
e67698f3c2
|
|||
|
4e305d67e3
|
@@ -8,7 +8,8 @@ COPY cmd ./cmd
|
|||||||
COPY internal ./internal
|
COPY internal ./internal
|
||||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /pawsql ./cmd/pawsql
|
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /pawsql ./cmd/pawsql
|
||||||
|
|
||||||
FROM gcr.io/distroless/static-debian12:nonroot
|
FROM alpine:3.21
|
||||||
|
RUN apk add --no-cache ca-certificates docker-cli
|
||||||
COPY --from=build /pawsql /usr/local/bin/pawsql
|
COPY --from=build /pawsql /usr/local/bin/pawsql
|
||||||
ENTRYPOINT ["/usr/local/bin/pawsql"]
|
ENTRYPOINT ["/usr/local/bin/pawsql"]
|
||||||
CMD ["--config", "/etc/pawsql/Barkfile"]
|
CMD ["--config", "/etc/pawsql/Barkfile"]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ PawSQL is a TLS-terminating PostgreSQL router. It accepts PostgreSQL clients on
|
|||||||
|
|
||||||
- Go 1.24 or later to build and run PawSQL natively.
|
- Go 1.24 or later to build and run PawSQL natively.
|
||||||
- Docker Engine and a usable `docker` CLI to build the PawSQL image. PawSQL also needs them in its own execution environment when it manages PostgreSQL containers.
|
- Docker Engine and a usable `docker` CLI to build the PawSQL image. PawSQL also needs them in its own execution environment when it manages PostgreSQL containers.
|
||||||
- A TLS certificate and private key readable by PawSQL. The certificate must cover every hostname clients use for SNI routing.
|
- Optional: a TLS certificate and private key readable by PawSQL. Omit the `tls` block to serve plaintext PostgreSQL; with TLS, the certificate must cover every hostname clients use for SNI routing.
|
||||||
- Docker Engine access for each `postgres` route. Managed database images are limited to `postgres:16`, `postgres:17`, and `postgres:18`.
|
- Docker Engine access for each `postgres` route. Managed database images are limited to `postgres:16`, `postgres:17`, and `postgres:18`.
|
||||||
|
|
||||||
## Build, configure, and run
|
## Build, configure, and run
|
||||||
@@ -33,10 +33,11 @@ docker build -t pawsql .
|
|||||||
docker run --rm --publish 5432:5432 \
|
docker run --rm --publish 5432:5432 \
|
||||||
--volume "$PWD/Barkfile:/etc/pawsql/Barkfile:ro" \
|
--volume "$PWD/Barkfile:/etc/pawsql/Barkfile:ro" \
|
||||||
--volume "$PWD/tls:/etc/pawsql/tls:ro" \
|
--volume "$PWD/tls:/etc/pawsql/tls:ro" \
|
||||||
|
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||||
pawsql
|
pawsql
|
||||||
```
|
```
|
||||||
|
|
||||||
The provided image contains only PawSQL and is suitable for external `upstream` routes. Managed PostgreSQL routes require native PawSQL or a custom image that supplies a Docker CLI and access to the Docker Engine, typically through the Docker socket.
|
The supplied image includes the Docker CLI so managed `postgres` routes can create, start, and stop their containers through the mounted Docker socket. The socket grants PawSQL root-equivalent control of the Docker host; mount it only for trusted Barkfiles and trusted administrators.
|
||||||
|
|
||||||
## Barkfile
|
## Barkfile
|
||||||
|
|
||||||
@@ -82,13 +83,15 @@ docker compose up --build
|
|||||||
|
|
||||||
## Routing and TLS
|
## Routing and TLS
|
||||||
|
|
||||||
PawSQL requires PostgreSQL's SSL negotiation and terminates client TLS before proxying PostgreSQL bytes to the selected upstream.
|
When the Barkfile configures `tls`, PawSQL handles PostgreSQL's SSL negotiation and terminates client TLS before proxying PostgreSQL bytes to the selected upstream:
|
||||||
|
|
||||||
- **With SNI:** PawSQL uses the TLS server name to select an exact configured `hostname` match. Hostname matching is case-insensitive and ignores a trailing dot. An unknown SNI name is rejected; PawSQL does not fall back to a database-name route when SNI is present.
|
- **With SNI:** PawSQL uses the TLS server name to select an exact configured `hostname` match. Hostname matching is case-insensitive and ignores a trailing dot. An unknown SNI name is rejected; PawSQL does not fall back to a database-name route when SNI is present.
|
||||||
- **Without SNI:** After TLS is established, PawSQL reads the PostgreSQL startup message and selects the route whose `database` name exactly matches the requested PostgreSQL database. This makes a route without `hostname` usable by non-SNI clients.
|
- **Without SNI:** After TLS is established, PawSQL reads the PostgreSQL startup message and selects the route whose `database` name exactly matches the requested PostgreSQL database. This makes a route without `hostname` usable by non-SNI clients.
|
||||||
|
|
||||||
Use a certificate trusted by clients and containing the SNI hostname they present. Clients that do not send SNI must request the configured database route name.
|
Use a certificate trusted by clients and containing the SNI hostname they present. Clients that do not send SNI must request the configured database route name.
|
||||||
|
|
||||||
|
Without a `tls` block, PawSQL serves plaintext PostgreSQL: clients connect without SSL negotiation, and routes are selected only by database name. Hostname routing is unavailable because it relies on TLS SNI.
|
||||||
|
|
||||||
## Managed PostgreSQL lifecycle
|
## Managed PostgreSQL lifecycle
|
||||||
|
|
||||||
Managed PostgreSQL is lazy: PawSQL creates or starts its `pawsql-<database>` container only when a client selects that route, waits for PostgreSQL to accept connections, then proxies the session. PawSQL stops managed containers but does not remove their data volumes.
|
Managed PostgreSQL is lazy: PawSQL creates or starts its `pawsql-<database>` container only when a client selects that route, waits for PostgreSQL to accept connections, then proxies the session. PawSQL stops managed containers but does not remove their data volumes.
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ func run(args []string, logger *slog.Logger) error {
|
|||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return fmt.Errorf("invalid configuration: %w", err)
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
}
|
}
|
||||||
certificate, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
|
var tlsConfig *tls.Config
|
||||||
if err != nil {
|
if cfg.TLS.CertFile != "" || cfg.TLS.KeyFile != "" {
|
||||||
return fmt.Errorf("load TLS certificate and key: %w", err)
|
certificate, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("load TLS certificate and key: %w", err)
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{certificate}}
|
||||||
|
} else {
|
||||||
|
logger.Info("TLS is not configured; clients connect without SSL and routes are selected by database name")
|
||||||
}
|
}
|
||||||
if validateOnly {
|
if validateOnly {
|
||||||
logger.Info("configuration is valid", "config", configPath)
|
logger.Info("configuration is valid", "config", configPath)
|
||||||
@@ -51,7 +57,7 @@ func run(args []string, logger *slog.Logger) error {
|
|||||||
return fmt.Errorf("build route resolver: %w", err)
|
return fmt.Errorf("build route resolver: %w", err)
|
||||||
}
|
}
|
||||||
resolver := postgres.NewResolver(staticResolver, cfg.Databases, postgres.NewProvisioner(logger))
|
resolver := postgres.NewResolver(staticResolver, cfg.Databases, postgres.NewProvisioner(logger))
|
||||||
routingServer, err := server.New(&tls.Config{Certificates: []tls.Certificate{certificate}}, resolver, logger)
|
routingServer, err := server.New(tlsConfig, resolver, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -2,4 +2,4 @@ module github.com/barkstack/pawsql
|
|||||||
|
|
||||||
go 1.24
|
go 1.24
|
||||||
|
|
||||||
require cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.0.0
|
require cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.1.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,2 +1,2 @@
|
|||||||
cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.0.0 h1:81V2fr9ln2WNqA2JHb40fvav0aW+5OQAOsnuhMkuRes=
|
cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.1.0 h1:ULdpvY1VC1d5M8VHx8SqacVLmk5SgvDYABDuzewebjc=
|
||||||
cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.0.0/go.mod h1:UnKTlB8ifO3cmsrkh2LDAM+Y2ipaCrBeiURwrSRPPe4=
|
cloud.campbellwireless.net/git/barkstack/barkfile-parser v1.1.0/go.mod h1:UnKTlB8ifO3cmsrkh2LDAM+Y2ipaCrBeiURwrSRPPe4=
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -13,7 +15,10 @@ const (
|
|||||||
sslRequestCode uint32 = 80877103
|
sslRequestCode uint32 = 80877103
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrNotSSLRequest = errors.New("expected PostgreSQL SSLRequest")
|
var (
|
||||||
|
ErrNotSSLRequest = errors.New("expected PostgreSQL SSLRequest")
|
||||||
|
ErrNotNegotiation = errors.New("not a PostgreSQL SSL or GSSENC negotiation request")
|
||||||
|
)
|
||||||
|
|
||||||
// ReadSSLRequest validates the eight-byte PostgreSQL SSL negotiation request.
|
// ReadSSLRequest validates the eight-byte PostgreSQL SSL negotiation request.
|
||||||
func ReadSSLRequest(reader io.Reader) error {
|
func ReadSSLRequest(reader io.Reader) error {
|
||||||
@@ -30,6 +35,42 @@ func ReadSSLRequest(reader io.Reader) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NegotiatePlainPostgreSQL handles a plaintext server's first protocol exchange
|
||||||
|
// and returns the connection positioned at the StartupMessage. When the client
|
||||||
|
// opens with an SSLRequest or GSSENCRequest it answers 'N', the standard
|
||||||
|
// PostgreSQL decline, so the client retries without encryption.
|
||||||
|
func NegotiatePlainPostgreSQL(connection net.Conn) (net.Conn, error) {
|
||||||
|
var preamble [8]byte
|
||||||
|
if err := connection.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
|
||||||
|
return connection, fmt.Errorf("set negotiation deadline: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(connection, preamble[:]); err != nil {
|
||||||
|
return connection, fmt.Errorf("read PostgreSQL negotiation preamble: %w", err)
|
||||||
|
}
|
||||||
|
if err := connection.SetReadDeadline(time.Time{}); err != nil {
|
||||||
|
return connection, fmt.Errorf("clear negotiation deadline: %w", err)
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint32(preamble[0:4])
|
||||||
|
code := binary.BigEndian.Uint32(preamble[4:8])
|
||||||
|
if length == sslRequestLength && (code == sslRequestCode || code == gssEncryptionCode) {
|
||||||
|
if err := declineSSL(connection); err != nil {
|
||||||
|
return connection, err
|
||||||
|
}
|
||||||
|
return connection, nil
|
||||||
|
}
|
||||||
|
if length < 8 || length > maxStartupMessageSize {
|
||||||
|
return connection, fmt.Errorf("%w: invalid length %d", ErrInvalidStartupMessage, length)
|
||||||
|
}
|
||||||
|
return Replay(connection, preamble[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func declineSSL(connection net.Conn) error {
|
||||||
|
if _, err := connection.Write([]byte{'N'}); err != nil {
|
||||||
|
return fmt.Errorf("write PostgreSQL SSL decline: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SSLRequest returns the exact client preamble used to request TLS from PostgreSQL.
|
// SSLRequest returns the exact client preamble used to request TLS from PostgreSQL.
|
||||||
// It exists to make protocol-level tests and clients unambiguous.
|
// It exists to make protocol-level tests and clients unambiguous.
|
||||||
func SSLRequest() [8]byte {
|
func SSLRequest() [8]byte {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
startupProtocolVersion uint32 = 196608
|
startupProtocolVersion uint32 = 196608
|
||||||
maxStartupMessageSize = 64 << 10
|
maxStartupMessageSize uint32 = 64 << 10
|
||||||
|
gssEncryptionCode uint32 = 80877104
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -36,22 +36,24 @@ type Server struct {
|
|||||||
handlers sync.WaitGroup
|
handlers sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates server dependencies and applies safe protocol defaults.
|
// New validates server dependencies and applies safe protocol defaults. A nil
|
||||||
|
// TLS configuration selects plaintext mode: PostgreSQL SSL negotiation is not
|
||||||
|
// offered and routes are selected by database name only.
|
||||||
func New(tlsConfig *tls.Config, resolver router.BackendResolver, logger *slog.Logger) (*Server, error) {
|
func New(tlsConfig *tls.Config, resolver router.BackendResolver, logger *slog.Logger) (*Server, error) {
|
||||||
if tlsConfig == nil {
|
|
||||||
return nil, errors.New("TLS configuration is required")
|
|
||||||
}
|
|
||||||
if resolver == nil {
|
if resolver == nil {
|
||||||
return nil, errors.New("backend resolver is required")
|
return nil, errors.New("backend resolver is required")
|
||||||
}
|
}
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = slog.Default()
|
logger = slog.Default()
|
||||||
}
|
}
|
||||||
copy := tlsConfig.Clone()
|
if tlsConfig != nil {
|
||||||
if copy.MinVersion == 0 {
|
copy := tlsConfig.Clone()
|
||||||
copy.MinVersion = tls.VersionTLS12
|
if copy.MinVersion == 0 {
|
||||||
|
copy.MinVersion = tls.VersionTLS12
|
||||||
|
}
|
||||||
|
tlsConfig = copy
|
||||||
}
|
}
|
||||||
return &Server{TLSConfig: copy, Resolver: resolver, Logger: logger, HandshakeTimeout: defaultHandshakeTimeout, ResolveTimeout: defaultResolveTimeout, DialTimeout: defaultDialTimeout}, nil
|
return &Server{TLSConfig: tlsConfig, Resolver: resolver, Logger: logger, HandshakeTimeout: defaultHandshakeTimeout, ResolveTimeout: defaultResolveTimeout, DialTimeout: defaultDialTimeout}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve accepts connections until Shutdown closes its listener.
|
// Serve accepts connections until Shutdown closes its listener.
|
||||||
@@ -114,26 +116,28 @@ func (s *Server) handle(connection net.Conn) {
|
|||||||
handshakeTimeout = defaultHandshakeTimeout
|
handshakeTimeout = defaultHandshakeTimeout
|
||||||
}
|
}
|
||||||
_ = connection.SetDeadline(time.Now().Add(handshakeTimeout))
|
_ = connection.SetDeadline(time.Now().Add(handshakeTimeout))
|
||||||
if err := pgwire.ReadSSLRequest(connection); err != nil {
|
|
||||||
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "invalid_ssl_request", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := connection.Write([]byte{'S'}); err != nil {
|
|
||||||
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "ssl_response_failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConnection := tls.Server(connection, s.TLSConfig)
|
|
||||||
if err := tlsConnection.Handshake(); err != nil {
|
|
||||||
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "tls_handshake_failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = tlsConnection.SetDeadline(time.Time{})
|
|
||||||
sni = normalizeHostname(tlsConnection.ConnectionState().ServerName)
|
|
||||||
var (
|
var (
|
||||||
proxyClient net.Conn = tlsConnection
|
proxyClient net.Conn = connection
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
if s.TLSConfig != nil {
|
||||||
|
if err := pgwire.ReadSSLRequest(connection); err != nil {
|
||||||
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "invalid_ssl_request", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := connection.Write([]byte{'S'}); err != nil {
|
||||||
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "ssl_response_failed", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tlsConnection := tls.Server(connection, s.TLSConfig)
|
||||||
|
if err := tlsConnection.Handshake(); err != nil {
|
||||||
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "tls_handshake_failed", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = tlsConnection.SetDeadline(time.Time{})
|
||||||
|
sni = normalizeHostname(tlsConnection.ConnectionState().ServerName)
|
||||||
|
proxyClient = tlsConnection
|
||||||
|
}
|
||||||
resolveTimeout := s.ResolveTimeout
|
resolveTimeout := s.ResolveTimeout
|
||||||
if resolveTimeout <= 0 {
|
if resolveTimeout <= 0 {
|
||||||
resolveTimeout = defaultResolveTimeout
|
resolveTimeout = defaultResolveTimeout
|
||||||
@@ -141,6 +145,14 @@ func (s *Server) handle(connection net.Conn) {
|
|||||||
resolveContext, cancelResolve := context.WithTimeout(context.Background(), resolveTimeout)
|
resolveContext, cancelResolve := context.WithTimeout(context.Background(), resolveTimeout)
|
||||||
defer cancelResolve()
|
defer cancelResolve()
|
||||||
|
|
||||||
|
if s.TLSConfig == nil {
|
||||||
|
negotiated, negotiateErr := pgwire.NegotiatePlainPostgreSQL(connection)
|
||||||
|
if negotiateErr != nil {
|
||||||
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "invalid_negotiation", "error", negotiateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyClient = negotiated
|
||||||
|
}
|
||||||
if sni != "" {
|
if sni != "" {
|
||||||
backend, err = s.Resolver.Resolve(resolveContext, sni)
|
backend, err = s.Resolver.Resolve(resolveContext, sni)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,7 +160,7 @@ func (s *Server) handle(connection net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startup, startupErr := pgwire.ReadStartupMessage(tlsConnection)
|
startup, startupErr := pgwire.ReadStartupMessage(proxyClient)
|
||||||
if startupErr != nil {
|
if startupErr != nil {
|
||||||
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "invalid_startup_message", "error", startupErr)
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "invalid_startup_message", "error", startupErr)
|
||||||
return
|
return
|
||||||
@@ -158,7 +170,7 @@ func (s *Server) handle(connection net.Conn) {
|
|||||||
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "unknown_database", "error", err)
|
s.Logger.Warn("connection rejected", "remote_address", remote, "result", "unknown_database", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
proxyClient = pgwire.Replay(tlsConnection, startup.Bytes())
|
proxyClient = pgwire.Replay(proxyClient, startup.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
if leases, ok := s.Resolver.(router.ConnectionLeaseManager); ok {
|
if leases, ok := s.Resolver.(router.ConnectionLeaseManager); ok {
|
||||||
|
|||||||
@@ -195,6 +195,141 @@ func TestServerRoutesNoSNIByConfiguredDatabase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServerRoutesPlaintextConnectionsByDatabase(t *testing.T) {
|
||||||
|
backendListener := listen(t)
|
||||||
|
defer backendListener.Close()
|
||||||
|
backendDatabase := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
connection, err := backendListener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
startup, err := pgwire.ReadStartupMessage(connection)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backendDatabase <- startup.Database
|
||||||
|
_, _ = connection.Write([]byte("backend"))
|
||||||
|
}()
|
||||||
|
|
||||||
|
staticResolver, err := router.NewStaticResolver([]config.DatabaseConfig{{
|
||||||
|
Name: "analytics",
|
||||||
|
Upstream: backendListener.Addr().String(),
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
routingServer, err := New(nil, staticResolver, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
listener := listen(t)
|
||||||
|
defer func() {
|
||||||
|
_ = routingServer.Shutdown()
|
||||||
|
routingServer.Wait()
|
||||||
|
_ = listener.Close()
|
||||||
|
}()
|
||||||
|
go func() { _ = routingServer.Serve(listener) }()
|
||||||
|
|
||||||
|
connection, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
if _, err := connection.Write(startupMessage("analytics")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response := make([]byte, len("backend"))
|
||||||
|
if _, err := io.ReadFull(connection, response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(response) != "backend" {
|
||||||
|
t.Fatalf("proxied response = %q", response)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case database := <-backendDatabase:
|
||||||
|
if database != "analytics" {
|
||||||
|
t.Errorf("backend database = %q, want analytics", database)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("backend did not receive the plaintext StartupMessage")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerDeclinesSSLNegotiationInPlaintextMode(t *testing.T) {
|
||||||
|
backendListener := listen(t)
|
||||||
|
defer backendListener.Close()
|
||||||
|
backendDatabase := make(chan string, 1)
|
||||||
|
go func() {
|
||||||
|
connection, err := backendListener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
startup, err := pgwire.ReadStartupMessage(connection)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backendDatabase <- startup.Database
|
||||||
|
_, _ = connection.Write([]byte("backend"))
|
||||||
|
}()
|
||||||
|
|
||||||
|
staticResolver, err := router.NewStaticResolver([]config.DatabaseConfig{{
|
||||||
|
Name: "analytics",
|
||||||
|
Upstream: backendListener.Addr().String(),
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
routingServer, err := New(nil, staticResolver, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
listener := listen(t)
|
||||||
|
defer func() {
|
||||||
|
_ = routingServer.Shutdown()
|
||||||
|
routingServer.Wait()
|
||||||
|
_ = listener.Close()
|
||||||
|
}()
|
||||||
|
go func() { _ = routingServer.Serve(listener) }()
|
||||||
|
|
||||||
|
connection, err := net.Dial("tcp", listener.Addr().String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
request := pgwire.SSLRequest()
|
||||||
|
if _, err := connection.Write(request[:]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(connection, response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if response[0] != 'N' {
|
||||||
|
t.Fatalf("SSL response = %q, want N", response)
|
||||||
|
}
|
||||||
|
if _, err := connection.Write(startupMessage("analytics")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
response = make([]byte, len("backend"))
|
||||||
|
if _, err := io.ReadFull(connection, response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if string(response) != "backend" {
|
||||||
|
t.Fatalf("proxied response = %q", response)
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case database := <-backendDatabase:
|
||||||
|
if database != "analytics" {
|
||||||
|
t.Errorf("backend database = %q, want analytics", database)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("backend did not receive the StartupMessage after SSL decline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func startupMessage(database string) []byte {
|
func startupMessage(database string) []byte {
|
||||||
body := make([]byte, 4)
|
body := make([]byte, 4)
|
||||||
binary.BigEndian.PutUint32(body, 196608)
|
binary.BigEndian.PutUint32(body, 196608)
|
||||||
|
|||||||
Reference in New Issue
Block a user