Skip to content

Commit

Permalink
postgres: New matcher module (#186)
Browse files Browse the repository at this point in the history
* L4 Matcher based on SSH module and published resources

- Detects Postgres connections based on `StartupMessage` and `SSLRequest` message formats defined in the protocol documentation

* Include Postgres module by default

* Add to README

* Add license notice

* Unexport identifiers that aren't used outside the package

* Run gofmt

* Finish sentence
  • Loading branch information
metafeather committed May 3, 2024
1 parent c5d815d commit 145ec36
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Current matchers:
- **layer4.matchers.http** - matches connections that start with HTTP requests. In addition, any [`http.matchers` modules](https://caddyserver.com/docs/modules/) can be used for matching on HTTP-specific properties of requests, such as header or path. Note that only the first request of each connection can be used for matching.
- **layer4.matchers.tls** - matches connections that start with TLS handshakes. In addition, any [`tls.handshake_match` modules](https://caddyserver.com/docs/modules/) can be used for matching on TLS-specific properties of the ClientHello, such as ServerName (SNI).
- **layer4.matchers.ssh** - matches connections that look like SSH connections.
- **layer4.matchers.postgres** - matches connections that look like Postgres connections.
- **layer4.matchers.ip** - matches connections based on remote IP (or CIDR range).
- **layer4.matchers.local_ip** - matches connections based on local IP (or CIDR range).
- **layer4.matchers.proxy_protocol** - matches connections that start with [HAPROXY proxy protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt).
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
_ "github.com/mholt/caddy-l4/layer4"
_ "github.com/mholt/caddy-l4/modules/l4echo"
_ "github.com/mholt/caddy-l4/modules/l4http"
_ "github.com/mholt/caddy-l4/modules/l4postgres"
_ "github.com/mholt/caddy-l4/modules/l4proxy"
_ "github.com/mholt/caddy-l4/modules/l4proxyprotocol"
_ "github.com/mholt/caddy-l4/modules/l4socks"
Expand Down
133 changes: 133 additions & 0 deletions modules/l4postgres/matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package l4postgres allows the L4 multiplexing of Postgres connections
//
// With thanks to docs and code published at these links:
// ref: https://github.com/mholt/caddy-l4/blob/master/modules/l4ssh/matcher.go
// ref: https://github.com/rueian/pgbroker/blob/master/message/startup_message.go
// ref: https://github.com/traefik/traefik/blob/master/pkg/server/router/tcp/postgres.go
// ref: https://ivdl.co.za/2024/03/02/pretending-to-be-postgresql-part-one-1/
// ref: https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-STARTUPMESSAGE
// ref: https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-SSLREQUEST
package l4postgres

import (
"encoding/binary"
"errors"
"io"

"github.com/caddyserver/caddy/v2"
"github.com/mholt/caddy-l4/layer4"
)

func init() {
caddy.RegisterModule(MatchPostgres{})
}

const (
// Magic number to identify a SSLRequest message
sslRequestCode = 80877103
// byte size of the message length field
initMessageSizeLength = 4
)

// Message provides readers for various types and
// updates the offset after each read
type message struct {
data []byte
offset uint32
}

func (b *message) ReadUint32() (r uint32) {
r = binary.BigEndian.Uint32(b.data[b.offset : b.offset+4])
b.offset += 4
return r
}

func (b *message) ReadString() (r string) {
end := b.offset
max := uint32(len(b.data))
for ; end != max && b.data[end] != 0; end++ {
}
r = string(b.data[b.offset:end])
b.offset = end + 1
return r
}

// NewMessageFromBytes wraps the raw bytes of a message to enable processing
func newMessageFromBytes(b []byte) *message {
return &message{data: b}
}

// StartupMessage contains the values parsed from the startup message
type startupMessage struct {
ProtocolVersion uint32
Parameters map[string]string
}

// MatchPostgres is able to match Postgres connections.
type MatchPostgres struct{}

// CaddyModule returns the Caddy module information.
func (MatchPostgres) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "layer4.matchers.postgres",
New: func() caddy.Module { return new(MatchPostgres) },
}
}

// Match returns true if the connection looks like the Postgres protocol.
func (m MatchPostgres) Match(cx *layer4.Connection) (bool, error) {
// Get bytes containing the message length
head := make([]byte, initMessageSizeLength)
if _, err := io.ReadFull(cx, head); err != nil {
return false, err
}

// Get actual message length
data := make([]byte, binary.BigEndian.Uint32(head)-initMessageSizeLength)
if _, err := io.ReadFull(cx, data); err != nil {
return false, err
}

b := newMessageFromBytes(data)

// Check if it is a SSLRequest
code := b.ReadUint32()
if code == sslRequestCode {
return true, nil
}

// Check supported protocol
if majorVersion := code >> 16; majorVersion < 3 {
return false, errors.New("pg protocol < 3.0 is not supported")
}

// Try parsing Postgres Params
startup := &startupMessage{ProtocolVersion: code, Parameters: make(map[string]string)}
for {
k := b.ReadString()
if k == "" {
break
}
startup.Parameters[k] = b.ReadString()
}
// TODO(metafeather): match on param values: user, database, options, etc

return len(startup.Parameters) > 0, nil
}

// Interface guard
var _ layer4.ConnMatcher = (*MatchPostgres)(nil)

0 comments on commit 145ec36

Please sign in to comment.