Skip to content

Commit 43a088f

Browse files
committed
feat: implement peer-based systemd unit authorization for Varlink service methods
1 parent a0ee1b7 commit 43a088f

10 files changed

Lines changed: 270 additions & 29 deletions

File tree

Cargo.lock

Lines changed: 43 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ sysinfo = "0.30"
2222
libc = "0.2"
2323
toml = "1.1.2"
2424
once_cell = "1.21.4"
25+
threadpool = "1.8.1"
2526

2627
[build-dependencies]
2728
varlink_generator = "13.0"

examples/config.sample.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,8 @@ diagnostics = 300
1414
# processes = ["Calculator"]
1515
# Hardware paths (udev paths) to ignore
1616
# devices = ["/sys/devices/virtual/input/input231"]
17+
18+
[auth]
19+
# systemd units authorized to access restricted interfaces (e.g. RGB Control, Controller Registration)
20+
# If this list is empty, validation is disabled.
21+
# authorized_units = ["openrgb.service"]

src/auth.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Peer authentication and identity for contextd
2+
//!
3+
//! Provides utilities to identify the calling process on a Unix socket
4+
//! and map it to a systemd unit for granular access control.
5+
6+
use std::cell::RefCell;
7+
use std::fs;
8+
use std::os::unix::io::AsRawFd;
9+
use std::os::unix::net::UnixStream;
10+
11+
thread_local! {
12+
/// Stores the credentials of the peer currently being handled by this thread.
13+
static CURRENT_PEER: RefCell<Option<PeerInfo>> = const { RefCell::new(None) };
14+
}
15+
16+
/// Information about the connected peer
17+
#[derive(Debug, Clone)]
18+
#[allow(dead_code)]
19+
pub struct PeerInfo {
20+
pub pid: i32,
21+
pub uid: u32,
22+
pub gid: u32,
23+
pub unit: Option<String>,
24+
}
25+
26+
impl PeerInfo {
27+
/// Extracts peer credentials from a UnixStream
28+
pub fn from_stream(stream: &UnixStream) -> Option<Self> {
29+
let fd = stream.as_raw_fd();
30+
let mut ucred = libc::ucred {
31+
pid: 0,
32+
uid: 0,
33+
gid: 0,
34+
};
35+
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
36+
37+
let res = unsafe {
38+
libc::getsockopt(
39+
fd,
40+
libc::SOL_SOCKET,
41+
libc::SO_PEERCRED,
42+
&mut ucred as *mut _ as *mut _,
43+
&mut len,
44+
)
45+
};
46+
47+
if res == 0 {
48+
let pid = ucred.pid;
49+
let unit = Self::get_unit_for_pid(pid);
50+
Some(Self {
51+
pid,
52+
uid: ucred.uid,
53+
gid: ucred.gid,
54+
unit,
55+
})
56+
} else {
57+
None
58+
}
59+
}
60+
61+
/// Attempts to find the systemd unit name for a given PID by reading its cgroup.
62+
fn get_unit_for_pid(pid: i32) -> Option<String> {
63+
let cgroup_path = format!("/proc/{}/cgroup", pid);
64+
if let Ok(content) = fs::read_to_string(cgroup_path) {
65+
// In cgroup v2, the format is "0::/path/to/unit"
66+
for line in content.lines() {
67+
if let Some(path) = line.strip_prefix("0::") {
68+
// Typical paths:
69+
// /system.slice/contextd.service
70+
// /user.slice/user-1000.slice/user@1000.service/app.slice/app-name.scope
71+
72+
// We want the most specific .service or .scope name
73+
let parts: Vec<&str> = path.split('/').collect();
74+
for part in parts.iter().rev() {
75+
if part.ends_with(".service") || part.ends_with(".scope") {
76+
return Some(part.to_string());
77+
}
78+
}
79+
}
80+
}
81+
}
82+
None
83+
}
84+
}
85+
86+
/// Sets the current peer info for the local thread.
87+
pub fn set_current_peer(info: Option<PeerInfo>) {
88+
CURRENT_PEER.with(|p| *p.borrow_mut() = info);
89+
}
90+
91+
/// Gets the current peer info for the local thread.
92+
pub fn get_current_peer() -> Option<PeerInfo> {
93+
CURRENT_PEER.with(|p| p.borrow().clone())
94+
}

src/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ pub struct Config {
1616
pub ttls: TtlConfig,
1717
#[serde(default)]
1818
pub blacklist: BlacklistConfig,
19+
#[serde(default)]
20+
pub auth: AuthConfig,
21+
}
22+
23+
/// Authentication settings for peer validation
24+
#[derive(Debug, Deserialize, Clone, Default)]
25+
pub struct AuthConfig {
26+
/// Systemd units authorized to access restricted interfaces (e.g. RGB Control)
27+
pub authorized_units: Vec<String>,
1928
}
2029

2130
/// TTL settings for various detectors (in seconds)

src/contextd.varlink

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,4 @@ method UnregisterController(pid: int) -> ()
7878
# Lists all active controller hints.
7979
method ListControllers() -> (controllers: []Controller)
8080

81+
error PermissionDenied(unit: string)

src/main.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ mod contextd {
88
#![allow(clippy::all, non_snake_case, non_camel_case_types, unused_imports)]
99
include!(concat!(env!("OUT_DIR"), "/contextd.rs"));
1010
}
11+
mod auth;
1112
mod config;
1213
mod detectors;
1314

1415
mod service;
1516

1617
mod rgb;
1718

18-
use std::sync::{Arc, RwLock};
19-
use varlink::VarlinkService;
20-
19+
use crate::auth::{PeerInfo, set_current_peer};
2120
use crate::detectors::controllers::manager::ControllerManager;
2221
use crate::detectors::diagnostics::manager::DiagnosticsManager;
2322
use crate::detectors::games::heroic::HeroicDetector;
@@ -27,6 +26,11 @@ use crate::detectors::games::steam::SteamDetector;
2726
use crate::detectors::hardware::manager::HardwareManager;
2827
use crate::detectors::hardware::udev::UdevDetector;
2928
use crate::service::ContextService;
29+
use std::io::BufReader;
30+
use std::os::unix::net::UnixListener;
31+
use std::sync::{Arc, RwLock};
32+
use threadpool::ThreadPool;
33+
use varlink::{ConnectionHandler, VarlinkService};
3034

3135
/// Helper to fix socket permissions and ownership
3236
fn spawn_permission_fixer(path: String, mode: u32, use_rgb_group: bool) {
@@ -183,13 +187,9 @@ fn main() -> anyhow::Result<()> {
183187
log::info!("Observer listening on {}", obs_addr);
184188
log::info!("Control listening on {}", ctrl_addr);
185189

186-
let config = varlink::ListenConfig {
187-
initial_worker_threads: 1,
188-
max_worker_threads: 128,
189-
idle_timeout: 0,
190-
..Default::default()
191-
};
192-
varlink::listen(control_service, ctrl_addr, &config)?;
190+
// For simplicity, we only run one blocking listener in the main thread.
191+
// In RGB mode, the Control interface is the primary listener.
192+
run_server(control_service, ctrl_addr)?;
193193
} else {
194194
log::info!("Starting Context Daemon in Core Mode...");
195195
let _ = std::fs::create_dir_all("/run/contextd/public");
@@ -224,13 +224,42 @@ fn main() -> anyhow::Result<()> {
224224
);
225225
log::info!("Core listening on {}", address);
226226

227-
let config = varlink::ListenConfig {
228-
initial_worker_threads: 1,
229-
max_worker_threads: 128,
230-
idle_timeout: 0,
231-
..Default::default()
232-
};
233-
varlink::listen(varlink_service, address, &config)?;
227+
run_server(varlink_service, address)?;
228+
}
229+
230+
Ok(())
231+
}
232+
233+
/// A custom Varlink server implementation that captures peer credentials.
234+
fn run_server(service: VarlinkService, address: &str) -> anyhow::Result<()> {
235+
let path = address.trim_start_matches("unix:");
236+
let listener = UnixListener::bind(path)?;
237+
let pool = ThreadPool::new(128);
238+
let service = Arc::new(service);
239+
240+
log::debug!("Custom Varlink server listening on {}", path);
241+
242+
for stream in listener.incoming() {
243+
match stream {
244+
Ok(stream) => {
245+
let service = Arc::clone(&service);
246+
let peer_info = PeerInfo::from_stream(&stream);
247+
248+
pool.execute(move || {
249+
set_current_peer(peer_info);
250+
let mut reader = BufReader::new(&stream);
251+
let mut writer = &stream;
252+
253+
if let Err(e) = service.handle(&mut reader, &mut writer, None) {
254+
log::debug!("Connection closed: {}", e);
255+
}
256+
set_current_peer(None);
257+
});
258+
}
259+
Err(e) => {
260+
log::error!("Error accepting connection: {}", e);
261+
}
262+
}
234263
}
235264

236265
Ok(())

src/rgb/control.varlink

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ error InvalidMatrixSize(width: int, height: int, data_len: int)
2929

3030
# A color component value is outside the valid range (0-255)
3131
error InvalidColorValue(component: string, value: int)
32+
error PermissionDenied(unit: string)

0 commit comments

Comments
 (0)