From 13e17e7c485c1698b7d38d9225374fb4c3d56212 Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Thu, 2 May 2024 16:35:58 +0100 Subject: [PATCH 1/7] L4 Matcher based on SSH module and published resources - Detects Postgres connections based on `StartupMessage` and `SSLRequest` message formats defined in the protocol documentation --- modules/l4postgres/matcher.go | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 modules/l4postgres/matcher.go diff --git a/modules/l4postgres/matcher.go b/modules/l4postgres/matcher.go new file mode 100644 index 0000000..1e67747 --- /dev/null +++ b/modules/l4postgres/matcher.go @@ -0,0 +1,117 @@ +// 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 ( + SSLRequestCode = 80877103 + 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 +} + +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 message length bytes + 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 a SSLRequest identified by magic number + 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) From b618359dd5aecf15f828cbfef89c592cec970e80 Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Thu, 2 May 2024 16:36:31 +0100 Subject: [PATCH 2/7] Include Postgres module by default --- imports.go | 1 + 1 file changed, 1 insertion(+) diff --git a/imports.go b/imports.go index 33e196e..24bbf22 100644 --- a/imports.go +++ b/imports.go @@ -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" From e2bcca6d388d280de2cead878f969a4e38b60240 Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Thu, 2 May 2024 16:37:00 +0100 Subject: [PATCH 3/7] Add to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 90bda9a..150e164 100644 --- a/README.md +++ b/README.md @@ -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). From 5358323b1523fed2e17a5ecc4d8b19e44ab4851e Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Thu, 2 May 2024 22:51:29 +0100 Subject: [PATCH 4/7] Add license notice --- modules/l4postgres/matcher.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/modules/l4postgres/matcher.go b/modules/l4postgres/matcher.go index 1e67747..c07afae 100644 --- a/modules/l4postgres/matcher.go +++ b/modules/l4postgres/matcher.go @@ -1,4 +1,18 @@ -// Allows the L4 multiplexing of Postgres connections +// 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 @@ -7,7 +21,6 @@ // 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 ( From 574e9241292cfa1fa513e1efc47f5bf0869d3df4 Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Thu, 2 May 2024 22:52:31 +0100 Subject: [PATCH 5/7] Unexport identifiers that aren't used outside the package --- modules/l4postgres/matcher.go | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/modules/l4postgres/matcher.go b/modules/l4postgres/matcher.go index c07afae..71fff46 100644 --- a/modules/l4postgres/matcher.go +++ b/modules/l4postgres/matcher.go @@ -37,24 +37,26 @@ func init() { } const ( - SSLRequestCode = 80877103 - InitMessageSizeLength = 4 + // Magic number to identify a SSLRequest message + sslRequestCode = 80877103 + // byte size of the + initMessageSizeLength = 4 ) // Message provides readers for various types and // updates the offset after each read -type Message struct{ +type message struct{ data []byte offset uint32 } -func (b *Message) ReadUint32() (r 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) { +func (b *message) ReadString() (r string) { end := b.offset max := uint32(len(b.data)) for ; end != max && b.data[end] != 0; end++ { @@ -64,12 +66,13 @@ func (b *Message) ReadString() (r string) { return r } -func NewMessageFromBytes(b []byte) *Message { - return &Message{data: b} +// 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 { +type startupMessage struct { ProtocolVersion uint32 Parameters map[string]string } @@ -87,23 +90,23 @@ func (MatchPostgres) CaddyModule() caddy.ModuleInfo { // Match returns true if the connection looks like the Postgres protocol. func (m MatchPostgres) Match(cx *layer4.Connection) (bool, error) { - // Get message length bytes - head := make([]byte, InitMessageSizeLength) + // 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) + data := make([]byte, binary.BigEndian.Uint32(head)-initMessageSizeLength) if _, err := io.ReadFull(cx, data); err != nil { return false, err } - b := NewMessageFromBytes(data) + b := newMessageFromBytes(data) - // Check if a SSLRequest identified by magic number + // Check if it is a SSLRequest code := b.ReadUint32() - if code == SSLRequestCode { + if code == sslRequestCode { return true, nil } @@ -113,7 +116,7 @@ func (m MatchPostgres) Match(cx *layer4.Connection) (bool, error) { } // Try parsing Postgres Params - startup := &StartupMessage{ProtocolVersion: code, Parameters: make(map[string]string)} + startup := &startupMessage{ProtocolVersion: code, Parameters: make(map[string]string)} for { k := b.ReadString() if k == "" { From e4da2eb72688ffe634c259664d8c11f62686c522 Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Fri, 3 May 2024 12:25:08 +0100 Subject: [PATCH 6/7] Run gofmt --- modules/l4postgres/matcher.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/l4postgres/matcher.go b/modules/l4postgres/matcher.go index 71fff46..2b00568 100644 --- a/modules/l4postgres/matcher.go +++ b/modules/l4postgres/matcher.go @@ -39,14 +39,14 @@ func init() { const ( // Magic number to identify a SSLRequest message sslRequestCode = 80877103 - // byte size of the + // byte size of the initMessageSizeLength = 4 ) -// Message provides readers for various types and +// Message provides readers for various types and // updates the offset after each read -type message struct{ - data []byte +type message struct { + data []byte offset uint32 } @@ -113,7 +113,7 @@ func (m MatchPostgres) Match(cx *layer4.Connection) (bool, error) { // 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)} From c3f1ef50f3dc13f60e1312907e41f40ac665bb0b Mon Sep 17 00:00:00 2001 From: "Liam Clancy (metafeather)" Date: Fri, 3 May 2024 20:08:59 +0100 Subject: [PATCH 7/7] Finish sentence --- modules/l4postgres/matcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/l4postgres/matcher.go b/modules/l4postgres/matcher.go index 2b00568..9bd7444 100644 --- a/modules/l4postgres/matcher.go +++ b/modules/l4postgres/matcher.go @@ -39,7 +39,7 @@ func init() { const ( // Magic number to identify a SSLRequest message sslRequestCode = 80877103 - // byte size of the + // byte size of the message length field initMessageSizeLength = 4 )