github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4postgres/matcher.go (about)

     1  // Copyright 2020 Matthew Holt
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package l4postgres allows the L4 multiplexing of Postgres connections
    16  //
    17  // With thanks to docs and code published at these links:
    18  // ref: https://github.com/mholt/caddy-l4/blob/master/modules/l4ssh/matcher.go
    19  // ref: https://github.com/rueian/pgbroker/blob/master/message/startup_message.go
    20  // ref: https://github.com/traefik/traefik/blob/master/pkg/server/router/tcp/postgres.go
    21  // ref: https://ivdl.co.za/2024/03/02/pretending-to-be-postgresql-part-one-1/
    22  // ref: https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-STARTUPMESSAGE
    23  // ref: https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-SSLREQUEST
    24  package l4postgres
    25  
    26  import (
    27  	"encoding/binary"
    28  	"errors"
    29  	"io"
    30  
    31  	"github.com/caddyserver/caddy/v2"
    32  	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    33  
    34  	"github.com/mholt/caddy-l4/layer4"
    35  )
    36  
    37  func init() {
    38  	caddy.RegisterModule(&MatchPostgres{})
    39  }
    40  
    41  const (
    42  	// Magic number to identify a SSLRequest message
    43  	sslRequestCode = 80877103
    44  	// byte size of the message length field
    45  	initMessageSizeLength = 4
    46  )
    47  
    48  // Message provides readers for various types and
    49  // updates the offset after each read
    50  type message struct {
    51  	data   []byte
    52  	offset uint32
    53  }
    54  
    55  func (b *message) ReadUint32() (r uint32) {
    56  	r = binary.BigEndian.Uint32(b.data[b.offset : b.offset+4])
    57  	b.offset += 4
    58  	return r
    59  }
    60  
    61  func (b *message) ReadString() (r string) {
    62  	end := b.offset
    63  	maximum := uint32(len(b.data))
    64  	for ; end != maximum && b.data[end] != 0; end++ {
    65  	}
    66  	r = string(b.data[b.offset:end])
    67  	b.offset = end + 1
    68  	return r
    69  }
    70  
    71  // NewMessageFromBytes wraps the raw bytes of a message to enable processing
    72  func newMessageFromBytes(b []byte) *message {
    73  	return &message{data: b}
    74  }
    75  
    76  // StartupMessage contains the values parsed from the startup message
    77  type startupMessage struct {
    78  	ProtocolVersion uint32
    79  	Parameters      map[string]string
    80  }
    81  
    82  // MatchPostgres is able to match Postgres connections.
    83  type MatchPostgres struct{}
    84  
    85  // CaddyModule returns the Caddy module information.
    86  func (*MatchPostgres) CaddyModule() caddy.ModuleInfo {
    87  	return caddy.ModuleInfo{
    88  		ID:  "layer4.matchers.postgres",
    89  		New: func() caddy.Module { return new(MatchPostgres) },
    90  	}
    91  }
    92  
    93  // Match returns true if the connection looks like the Postgres protocol.
    94  func (m *MatchPostgres) Match(cx *layer4.Connection) (bool, error) {
    95  	// Get bytes containing the message length
    96  	head := make([]byte, initMessageSizeLength)
    97  	if _, err := io.ReadFull(cx, head); err != nil {
    98  		return false, err
    99  	}
   100  
   101  	// Get actual message length
   102  	data := make([]byte, binary.BigEndian.Uint32(head)-initMessageSizeLength)
   103  	if _, err := io.ReadFull(cx, data); err != nil {
   104  		return false, err
   105  	}
   106  
   107  	b := newMessageFromBytes(data)
   108  
   109  	// Check if it is a SSLRequest
   110  	code := b.ReadUint32()
   111  	if code == sslRequestCode {
   112  		return true, nil
   113  	}
   114  
   115  	// Check supported protocol
   116  	if majorVersion := code >> 16; majorVersion < 3 {
   117  		return false, errors.New("pg protocol < 3.0 is not supported")
   118  	}
   119  
   120  	// Try parsing Postgres Params
   121  	startup := &startupMessage{ProtocolVersion: code, Parameters: make(map[string]string)}
   122  	for {
   123  		k := b.ReadString()
   124  		if k == "" {
   125  			break
   126  		}
   127  		startup.Parameters[k] = b.ReadString()
   128  	}
   129  	// TODO(metafeather): match on param values: user, database, options, etc
   130  
   131  	return len(startup.Parameters) > 0, nil
   132  }
   133  
   134  // UnmarshalCaddyfile sets up the MatchPostgres from Caddyfile tokens. Syntax:
   135  //
   136  //	postgres
   137  func (m *MatchPostgres) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
   138  	_, wrapper := d.Next(), d.Val() // consume wrapper name
   139  
   140  	// No same-line options are supported
   141  	if d.CountRemainingArgs() > 0 {
   142  		return d.ArgErr()
   143  	}
   144  
   145  	// No blocks are supported
   146  	if d.NextBlock(d.Nesting()) {
   147  		return d.Errf("malformed layer4 connection matcher '%s': blocks are not supported", wrapper)
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  // Interface guards
   154  var (
   155  	_ layer4.ConnMatcher    = (*MatchPostgres)(nil)
   156  	_ caddyfile.Unmarshaler = (*MatchPostgres)(nil)
   157  )