From bf59bd4c80fba6aa94ca4ccb84adc02cb6948cc9 Mon Sep 17 00:00:00 2001 From: Lol3rrr Date: Sat, 10 Oct 2026 14:36:21 +0200 Subject: [PATCH] Initial --- .gitignore | 1 + Cargo.lock | 7 ++ README.md | 2 + cargo.toml | 3 + messages/.gitignore | 1 + messages/Cargo.toml | 6 + messages/src/datatypes.rs | 50 ++++++++ messages/src/lib.rs | 31 +++++ messages/src/startup.rs | 229 +++++++++++++++++++++++++++++++++++ messages/tests/startup.rs | 100 +++++++++++++++ traces/README.md | 1 + traces/output/container.pcap | Bin 0 -> 2201 bytes traces/trace.sh | 27 +++++ 13 files changed, 458 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 README.md create mode 100644 cargo.toml create mode 100644 messages/.gitignore create mode 100644 messages/Cargo.toml create mode 100644 messages/src/datatypes.rs create mode 100644 messages/src/lib.rs create mode 100644 messages/src/startup.rs create mode 100644 messages/tests/startup.rs create mode 100644 traces/README.md create mode 100644 traces/output/container.pcap create mode 100644 traces/trace.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4afd746 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "messages" +version = "0.1.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4d61ac --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# sans-gres +A sans-io based implementation of the postgres protocol diff --git a/cargo.toml b/cargo.toml new file mode 100644 index 0000000..dd864c5 --- /dev/null +++ b/cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["messages"] +resolver = "3" diff --git a/messages/.gitignore b/messages/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/messages/.gitignore @@ -0,0 +1 @@ +/target diff --git a/messages/Cargo.toml b/messages/Cargo.toml new file mode 100644 index 0000000..cb279ba --- /dev/null +++ b/messages/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "messages" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/messages/src/datatypes.rs b/messages/src/datatypes.rs new file mode 100644 index 0000000..0cddb41 --- /dev/null +++ b/messages/src/datatypes.rs @@ -0,0 +1,50 @@ +//! [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-types.html#PROTOCOL-MESSAGE-TYPES) + +#[derive(Debug)] +pub enum DataTypeError { + Other(&'static str), +} + +pub trait DataType: Sized { + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), DataTypeError> where 'b1: 'b2; + + fn encoding_length(&self) -> usize; + fn encode(&self, writer: &mut W) where W: std::io::Write; +} + +impl DataType for i32 { + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), DataTypeError> where 'b1: 'b2 { + let relevant_data = &buffer[0..4]; + let value = i32::from_be_bytes(relevant_data.try_into().unwrap()); + + Ok((value, &buffer[4..])) + } + + fn encoding_length(&self) -> usize { + 4 + } + + fn encode(&self, writer: &mut W) where W: std::io::Write { + let content = self.to_be_bytes(); + writer.write_all(&content).unwrap(); + } +} + +impl DataType for String { + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), DataTypeError> where 'b1: 'b2 { + let str_length = buffer.iter().enumerate().find(|(_, v)| **v == 0).map(|(i, _)| i).unwrap(); + let raw_content = &buffer[0..str_length]; + let content = str::from_utf8(raw_content).unwrap(); + + Ok((content.to_owned(), &buffer[str_length+1..])) + } + + fn encoding_length(&self) -> usize { + self.len() + 1 + } + + fn encode(&self, writer: &mut W) where W: std::io::Write { + writer.write_all(self.as_bytes()).unwrap(); + writer.write_all(&[0]).unwrap(); + } +} diff --git a/messages/src/lib.rs b/messages/src/lib.rs new file mode 100644 index 0000000..5933bf9 --- /dev/null +++ b/messages/src/lib.rs @@ -0,0 +1,31 @@ +pub mod startup; +pub mod datatypes; + +#[derive(Debug)] +pub enum MessageParseError { + MismatchedId { + expected: u8, + found: u8, + }, + Other(&'static str), +} + +#[derive(Debug)] +pub enum EncodeError { + IO(std::io::Error), + Other(&'static str), +} + +pub trait Message: Sized { + const ID: Option; + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2; + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write; +} + +impl From for EncodeError { + fn from(value: std::io::Error) -> Self { + Self::IO(value) + } +} diff --git a/messages/src/startup.rs b/messages/src/startup.rs new file mode 100644 index 0000000..a10a59d --- /dev/null +++ b/messages/src/startup.rs @@ -0,0 +1,229 @@ +use crate::{EncodeError, Message, MessageParseError, datatypes::DataType}; + +/// Sent by the Frontend +/// +/// [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-SSLREQUEST) +#[derive(Debug, PartialEq)] +pub struct SSLRequest {} + +impl Message for SSLRequest { + const ID: Option = None; + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2 { + let (length, _) = i32::parse(buffer).unwrap(); + let (code, _) = i32::parse(&buffer[4..8]).unwrap(); + + if length != 8 || code != 80877103 { + return Err(MessageParseError::Other("Not an SSLRequest")); + } + + Ok((Self {}, &buffer[8..])) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + 8i32.encode(writer); + 80877103i32.encode(writer); + Ok(()) + } +} + +/// Sent by the Backend +#[derive(Debug, PartialEq)] +pub struct SSLResponse { + pub enabled: bool, +} + +impl Message for SSLResponse { + const ID: Option = None; + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2 { + let flag = buffer[0]; + + if flag != b'S' && flag != b'N' { + return Err(MessageParseError::Other("Invalid value for SSL Response")); + } + + Ok((Self { + enabled: flag == b'S', + }, &buffer[1..])) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + let flag = if self.enabled { b'S' } else { b'N' }; + writer.write_all(&[flag])?; + Ok(()) + } +} + +/// Sent by the frontend +/// +/// [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-STARTUPMESSAGE) +#[derive(Debug, PartialEq)] +pub struct Startup { + pub version: i32, + pub parameters: Vec<(String, String)>, +} + +impl Message for Startup { + const ID: Option = None; + + fn parse<'b1, 'b2>(in_buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), crate::MessageParseError> where 'b1: 'b2 { + let (length, _) = i32::parse(in_buffer).unwrap(); + + let buffer = &in_buffer[4..length as usize]; + + let (version, buffer) = i32::parse(buffer).unwrap(); + + let mut parameters = Vec::new(); + + let mut buffer = buffer; + while buffer.first().is_some_and(|v| *v != 0) { + let (name, tmp_buffer) = String::parse(buffer).unwrap(); + let (value, tmp_buffer) = String::parse(tmp_buffer).unwrap(); + + parameters.push((name, value)); + + buffer = tmp_buffer; + } + + if !buffer.first().is_some_and(|v| *v == 0) { + return Err(MessageParseError::Other("TODO")); + } + + Ok((Self { + version, + parameters, + }, &in_buffer[length as usize..])) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + let version_length = self.version.encoding_length(); + let parameters_length = self.parameters.iter().map(|(k, v)| k.encoding_length() + v.encoding_length()).sum::()+1; + + ((4+version_length+parameters_length) as i32).encode(writer); + self.version.encode(writer); + for (k, v) in self.parameters.iter() { + k.encode(writer); + v.encode(writer); + } + writer.write_all(&[0])?; + Ok(()) + } +} + +/// Sent by the backend +/// +/// [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-AUTHENTICATIONOK) +#[derive(Debug, PartialEq)] +pub struct AuthenticationOk {} + +impl Message for AuthenticationOk { + const ID: Option = Some(b'R'); + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2 { + let id = buffer[0]; + if id != Self::ID.unwrap() { + return Err(MessageParseError::MismatchedId { expected: Self::ID.unwrap(), found: id }); + } + + let (len, buffer) = i32::parse(&buffer[1..]).unwrap(); + if len != 8 { + return Err(MessageParseError::Other("Expected a length of 8")); + } + + let (code, buffer) = i32::parse(buffer).unwrap(); + if code != 0 { + return Err(MessageParseError::Other("Expectede the code to be 0")); + } + + Ok((Self {}, buffer)) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + writer.write_all(&[Self::ID.unwrap()])?; + 8i32.encode(writer); + 0i32.encode(writer); + Ok(()) + } +} + +/// Sent by the backend +/// +/// [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-PARAMETERSTATUS) +#[derive(Debug, PartialEq)] +pub struct ParameterStatus { + pub name: String, + pub value: String, +} + +impl Message for ParameterStatus { + const ID: Option = Some(b'S'); + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2 { + let id = buffer[0]; + if id != Self::ID.unwrap() { + return Err(MessageParseError::MismatchedId { expected: Self::ID.unwrap(), found: id }); + } + + let (len, buffer) = i32::parse(&buffer[1..]).unwrap(); + + let (name, buffer) = String::parse(buffer).unwrap(); + let (value, buffer) = String::parse(buffer).unwrap(); + + Ok((Self { name, value }, buffer)) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + let name_length = self.name.encoding_length(); + let value_length = self.value.encoding_length(); + + writer.write_all(&[Self::ID.unwrap()])?; + ((4+name_length+value_length) as i32).encode(writer); + self.name.encode(writer); + self.value.encode(writer); + Ok(()) + } +} + +/// Sent by the backend +/// +/// [Postgres Docs](https://www.postgresql.org/docs/current/protocol-message-formats.html#PROTOCOL-MESSAGE-FORMATS-BACKENDKEYDATA) +#[derive(Debug, PartialEq)] +pub struct BackendKeyData { + pub process_id: i32, + pub secret_key: Vec, +} + +impl Message for BackendKeyData { + const ID: Option = Some(b'K'); + + fn parse<'b1, 'b2>(buffer: &'b1 [u8]) -> Result<(Self, &'b2 [u8]), MessageParseError> where 'b1: 'b2 { + let id = buffer[0]; + if id != Self::ID.unwrap() { + return Err(MessageParseError::MismatchedId { expected: Self::ID.unwrap(), found: id }); + } + + let (len, buffer) = i32::parse(&buffer[1..]).unwrap(); + + let inner_buffer = &buffer[..len as usize - 4]; + + let (process_id, inner_buffer) = i32::parse(inner_buffer).unwrap(); + + Ok((Self { + process_id, + secret_key: inner_buffer.to_vec(), + }, &buffer[len as usize - 4..])) + } + + fn encode(&self, writer: &mut W) -> Result<(), EncodeError> where W: std::io::Write { + let process_len = self.process_id.encoding_length(); + let secret_key_length = self.secret_key.len(); + + writer.write_all(&[Self::ID.unwrap()])?; + ((4+process_len+secret_key_length) as i32).encode(writer); + self.process_id.encode(writer); + writer.write_all(self.secret_key.as_slice())?; + + Ok(()) + } +} diff --git a/messages/tests/startup.rs b/messages/tests/startup.rs new file mode 100644 index 0000000..b5c0e9c --- /dev/null +++ b/messages/tests/startup.rs @@ -0,0 +1,100 @@ +use messages::{Message, startup::{AuthenticationOk, BackendKeyData, ParameterStatus, SSLRequest, SSLResponse, Startup}}; + +#[test] +fn collected_startup() { + let raw_bytes: &[u8] = &[0x0, 0x0, 0x0, 0x3f, 0x0, 0x3, 0x0, 0x0, 0x75, 0x73, 0x65, 0x72, 0x0, 0x70, 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x0, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x0, 0x70, 0x6f, 0x73, 0x74, 0x67, 0x72, 0x65, 0x73, 0x0, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x0, 0x70, 0x73, 0x71, 0x6c, 0x0, 0x0]; + + let (parsed, rem) = Startup::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(Startup { + version: 196608, + parameters: vec![ + ("user".into(), "postgres".into()), + ("database".into(), "postgres".into()), + ("application_name".into(), "psql".into()), + ], + }, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} + +#[test] +fn collected_sslrequest() { + let raw_bytes: &[u8] = &[0x0, 0x0, 0x0, 0x8, 0x4, 0xd2, 0x16, 0x2f]; + + let (parsed, rem) = SSLRequest::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(SSLRequest {}, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} + +#[test] +fn collected_sslresponse() { + let raw_bytes: &[u8] = &[0x4e]; + + let (parsed, rem) = SSLResponse::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(SSLResponse { + enabled: false, + }, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} + +#[test] +fn collected_authenticationok() { + let raw_bytes: &[u8] = &[0x52, 0x0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0]; + + let (parsed, rem) = AuthenticationOk::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(AuthenticationOk {}, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} + +#[test] +fn collected_parameterstatus() { + let raw_bytes: &[u8] = &[0x53, 0x0, 0x0, 0x0, 0x17, 0x69, 0x6e, 0x5f, 0x68, 0x6f, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x62, 0x79, 0x0, 0x6f, 0x66, 0x66, 0x0]; + + let (parsed, rem) = ParameterStatus::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(ParameterStatus { + name: "in_hot_standby".into(), + value: "off".into(), + }, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} + +#[test] +fn collected_backendkeydata() { + let raw_bytes: &[u8] = &[0x4b, 0x0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x4b, 0x72, 0xb, 0xd3, 0xd6]; + + let (parsed, rem) = BackendKeyData::parse(raw_bytes).unwrap(); + + assert_eq!(&[] as &[u8], rem); + assert_eq!(BackendKeyData { + process_id: 75, + secret_key: vec![0x72, 0x0b, 0xd3, 0xd6], + }, parsed); + + let mut output = Vec::::new(); + parsed.encode(&mut output); + assert_eq!(raw_bytes, output.as_slice()); +} diff --git a/traces/README.md b/traces/README.md new file mode 100644 index 0000000..babf8b5 --- /dev/null +++ b/traces/README.md @@ -0,0 +1 @@ +# Traces diff --git a/traces/output/container.pcap b/traces/output/container.pcap new file mode 100644 index 0000000000000000000000000000000000000000..5a4fd883fbcb6cadc97abee957955f1888943133 GIT binary patch literal 2201 zcmb`IOH30{6ozkyszj71f)(79hlcVhRiZU6(9jYHKB6r?Fmb${-nP@UGvmyZ1mgl1 z;>LusFfk@3$Qn0pBqT->*|=5X&cuxy*DRN$4?IwdwO2`4#W=9W3%`Tu)n?zd0x z-U!4^O5r9hxcM{oIo+=bgo<4?Q&&W!-wCs&{~44c~8^gJP=nFn~EztX-M3njREZz z&@r81YxCBuZ3T$=*&_lW-Jr!~=|U1}(kg4(or4zT1W>?1Ck1-1cYitVN19Pt6FOLf zCfmV0IJj0x)1)qt+knM%1~`Y5_Ug`+QBaewT+=laBLZemHfRktXrYprCqQ`tDlna4 zkAYZSj6LEO1;r%G#X!|zQb$P6U^>xM!%XVTAaQEa7&TaFg=$(xmZ&MKim1>m{9-)J zSnuKArvmXqWlU#Sj=Kugo3tHYfPF2_LX!f#srD54a8_?Z*?KG;f%M3Vm{LvAFsTxc z%@8$_;OnijVzMODMQEOxau#H$3jd`g3U^bg!bsSZ{FBiUzIt*T{$HP_nTR=)aVSNG z*I-naQldu96zS+fb2|FGS}q12@Q0gaLo{+4)46Xpr|thHVZqaJmY}(eDVjP}3@ULS zbw=Z&s$@zU+6+mjSy6_@xYY(38oY3h6Fni2WwkmUx!&i!8oJBZJGl>3kBgG3Bvd^s zD@pK1hdJ=`xIR48^<^4mE8&r-H*k@k9rMow@)DLYone0^ zV%4G6%r2$F8-+#=O_xD?qENP-xP~P)lglc@C5VT(ix7l_V6A~4O0K{NTp!+Qk(A8& z1%2Lw-!PqFn{%pqpO!g=MlffY_t{_G=N%~P#(mfX)Lwki3dcC=DC|S&$iaQ+LCWLj o6)C$q1eTV;_~zMiO79lsRb>huY&9G;wr$Oz