intial version

This commit is contained in:
2026-02-24 18:05:44 +01:00
commit 50ae9d8128
8 changed files with 720 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

405
Cargo.lock generated Normal file
View File

@@ -0,0 +1,405 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "bindgen"
version = "0.69.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.117",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
]
[[package]]
name = "clap"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "clap_lex"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "glob"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pam"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ab553c52103edb295d8f7d6a3b593dc22a30b1fb99643c777a8f36915e285ba"
dependencies = [
"libc",
"memchr",
"pam-macros",
"pam-sys",
"users",
]
[[package]]
name = "pam-macros"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "pam-sys"
version = "1.0.0-alpha5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce9484729b3e52c0bacdc5191cb6a6a5f31ef4c09c5e4ab1209d3340ad9e997b"
dependencies = [
"bindgen",
"libc",
]
[[package]]
name = "pamsock"
version = "0.1.0"
dependencies = [
"anstyle",
"clap",
"pam",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "users"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
dependencies = [
"libc",
"log",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "pamsock"
version = "0.1.0"
edition = "2024"
[dependencies]
anstyle = "1.0.13"
clap = { version = "4.5.60", features = ["derive"] }
pam = "0.8.0"
[lints.clippy]
pedantic = { level = "deny", priority = -1 }
nursery = { level = "deny", priority = -1 }
perf = { level = "deny", priority = -1 }
style = { level = "deny", priority = -1 }
unwrap_used = "deny"

11
LICENSE Normal file
View File

@@ -0,0 +1,11 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# Pamsock
Exposes PAM authentication via a unix socket.
It's meanth to be secure enough to be mindlessly executed as root in its daemon form, because PAM requies root to interact with it. Code is simple enough for that reason, an audit should be really fast and dependency use is minimal for convenient stuff.
# Building
Requires `PAM_SERVICE` env variable at compile time (I recommend `login` for average distributions) to specify the name of the PAM service to login as.
# Docs
The protocol is dead simple and is documented with `cargo doc` under `lib::prot`. Also provides some utils to interact as a client with with library art of this project.
# License
Code licensed under the WTFPL.

68
src/args.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::ffi::OsString;
use clap::{Parser, Subcommand};
/// PAM though a Unix Socket
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(styles = get_clap_styles())]
pub struct Args {
#[arg(short, default_value = "pam")]
pub sock_abstract_name: OsString,
#[command(subcommand)]
pub cmd: Option<Subcommands>,
}
#[derive(Subcommand, Debug)]
pub enum Subcommands {
/// Attempts to login using the sock, note that the use of this is unsecure as any process can
/// find the password from the process list.
Login {
/// User to attempt login as
#[arg()]
user: String,
/// Password to attempt login with
#[arg()]
password: String,
},
}
fn get_clap_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
.usage(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))),
)
.header(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi256(anstyle::Ansi256Color(208)))),
)
.literal(
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
)
.invalid(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.error(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Red))),
)
.valid(
anstyle::Style::new()
.bold()
.underline()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
)
.placeholder(
anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Blue))),
)
}

126
src/lib.rs Normal file
View File

@@ -0,0 +1,126 @@
pub mod prot {
//! # Protocol
//!
//! The authentication protocol is dead simple for now, it is a client-server model in which
//! both parts send/receive data in the same format.
//!
//! ## Communication
//!
//! It simply works by expecting a set of data, and if, anything is unexpected, the communication aborts.
//!
//! ### Data De/Serialization
//!
//! Integer primitives are sent as bytes in Big Endian order.
//!
//! Strings are sent in 2 steps, first it sends the byte lenth. The byte length is flexible and
//! depends on the protocol, is sent first, then, that numbers of bytes is expected, and to be
//! in utf-8 format.
//!
//! The byte lenth type associated still be annotated by `String<N>` where N is the data type
//! that holds the length.
//!
//! ### Protocol
//!
//! The communication lacks versioning in the current state, as soon as the connection opens,
//! the client sends the user as a `String<u8>` and the password as a `String<u8>`. Numbers are
//! this small because higher users or passwords are rare or even impossible and this prevent
//! memory allocation abuse.
//!
//! Then the server responds with a single [`u8`], the given value is then mapped to the enum
//! [`ServerResponse`].
/// Represents the status after a login attempt
#[repr(u8)]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ServerResponse {
/// Something went wrong in the server, e.g. the server failed pam client initialization
ServerError = 0,
/// The user is locked due to too many failed locin attempts. One must not rely on these,
/// it's common for lockers to not report the lock status and just report as a [`Failed`]
/// state.
///
/// [`Failed`]: #variant.Failed
Locked = 1,
/// The authentication likely went through but failed
Failed = 2,
/// The authentication succeeded
Succeeded = 3,
}
impl ServerResponse {
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
#[must_use]
pub const fn from_u8(b: u8) -> Option<Self> {
match b {
0 => Some(Self::ServerError),
1 => Some(Self::Locked),
2 => Some(Self::Failed),
3 => Some(Self::Succeeded),
_ => None,
}
}
}
#[must_use]
pub fn attempt_login(
mut stream: std::os::unix::net::UnixStream,
user: &str,
passwd: &str,
) -> Option<ServerResponse> {
ucom::write_str(&mut stream, user)?;
ucom::write_str(&mut stream, passwd)?;
ServerResponse::from_u8(ucom::read_u8(&mut stream)?)
}
pub mod ucom {
//! # µcom
//!
//! Small module to serialize basic primitives in a very basic form.
use std::io::{Read, Write};
pub fn read_duple(from: &mut impl Read) -> Option<(String, String)> {
let user = read_str(from)?;
let pass = read_str(from)?;
Some((user, pass))
}
pub fn read_str(from: &mut impl Read) -> Option<String> {
let size = read_u8(from)? as usize;
let mut buf = vec![0; size];
from.read_exact(&mut buf).ok()?;
String::from_utf8(buf).ok()
}
pub fn read_u8(from: &mut impl Read) -> Option<u8> {
let mut size_buf = [0; 1];
from.read_exact(&mut size_buf).ok()?;
Some(u8::from_be_bytes(size_buf))
}
pub fn write_duple(into: &mut impl Write, user: &str, pass: &str) -> Option<()> {
write_str(into, user)?;
write_str(into, pass)?;
Some(())
}
pub fn write_str(into: &mut impl Write, str: &str) -> Option<()> {
let buf = str.as_bytes();
write_u8(into, buf.len().try_into().ok()?)?;
into.write_all(buf).ok()
}
pub fn write_u8(into: &mut impl Write, u8: u8) -> Option<()> {
let size_buf = u8.to_be_bytes();
into.write_all(&size_buf).ok()
}
}
}

76
src/main.rs Normal file
View File

@@ -0,0 +1,76 @@
use std::{
os::{
linux::net::SocketAddrExt,
unix::{
ffi::OsStrExt,
net::{SocketAddr, UnixListener, UnixStream},
},
},
thread,
};
use clap::Parser;
use pamsock::prot::{self, ServerResponse};
pub mod args;
const SERVICE_NAME: &str = env!("PAM_SERVICE");
fn conn(mut stream: UnixStream) -> Option<()> {
fn authenticate(user: String, passwd: String) -> ServerResponse {
let Ok(mut auth) = pam::Client::with_password(SERVICE_NAME) else {
return ServerResponse::ServerError;
};
auth.conversation_mut().set_credentials(user, passwd);
match auth.authenticate() {
Ok(()) => ServerResponse::Succeeded,
// TODO: check that [`ServerResponse::Locked`] works at lasst on my machine and is no
// other err core (it doesnt)
Err(pam::PamError(pam::PamReturnCode::MaxTries)) => ServerResponse::Locked,
Err(_) => ServerResponse::Failed,
}
}
let (user, passwd) = prot::ucom::read_duple(&mut stream)?;
let res = authenticate(user, passwd);
prot::ucom::write_u8(&mut stream, res.as_u8());
Some(())
}
#[link(name = "c")]
unsafe extern "C" {
#[must_use]
safe fn geteuid() -> u32;
}
fn main() -> std::io::Result<()> {
let args = args::Args::parse();
match args.cmd {
Some(args::Subcommands::Login { user, password }) => {
let addr = SocketAddr::from_abstract_name(args.sock_abstract_name.as_bytes())?;
let sock = UnixStream::connect_addr(&addr)?;
println!("{:?}", prot::attempt_login(sock, &user, &password));
}
None => {
if geteuid() != 0 {
eprintln!("\x1b[1;31mmust run as root\x1b[0m");
return Ok(());
}
let addr = SocketAddr::from_abstract_name(args.sock_abstract_name.as_bytes())?;
let listener = UnixListener::bind_addr(&addr)?;
for stream in listener.incoming() {
if let Ok(stream) = stream {
thread::spawn(|| conn(stream));
}
}
}
}
Ok(())
}