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 )