use fez::protocol::frame::{read_frame, write_frame, Frame};
use serde_json::{json, Value};
use std::io::{self, Write};
const SESSION_PATH: &str = "/org/rpm/dnf/v0/session/fake";
fn dnf_package(name: &str, evr: &str, arch: &str, repo_id: &str, install_size: u64) -> Value {
json!({
"name": {"t":"s","v":name},
"evr": {"t":"s","v":evr},
"arch": {"t":"s","v":arch},
"repo_id": {"t":"s","v":repo_id},
"install_size": {"t":"t","v":install_size},
"summary": {"t":"s","v":format!("{name} package")},
})
}
fn reject_unwrapped_options(args: &[Value], id: &Value) -> Option<Value> {
let opts = args.last()?.as_object()?;
for (key, val) in opts {
let wrapped = val
.as_object()
.is_some_and(|o| o.contains_key("t") && o.contains_key("v"));
if !wrapped {
return Some(json!({"error":[
"org.freedesktop.DBus.Error.InvalidArgs",
[format!(
"a{{sv}} value for key {key:?} is not a variant ({{\"t\",\"v\"}}); \
cockpit-bridge would raise a marshalling TypeError"
)]
],"id": id}));
}
}
None
}
fn dnf_reply(method: &str, iface: &str, id: &Value) -> Value {
match method {
"open_session" => {
if std::env::var_os("FEZ_FAKE_NO_DNF5").is_some() {
json!({"error":[
"org.freedesktop.DBus.Error.ServiceUnknown",
["The name org.rpm.dnf.v0 was not provided by any .service files"]
],"id": id})
} else {
json!({"reply":[[SESSION_PATH]],"id": id})
}
}
"list" if iface.ends_with(".rpm.Repo") => json!({"reply":[[[
dnf_repo("fedora", "Fedora", true),
dnf_repo("updates-testing", "Fedora - Testing", false),
]]],"id": id}),
"list" => {
let packages = match std::env::var("FEZ_FAKE_PACKAGE_COUNT")
.ok()
.and_then(|s| s.parse::<usize>().ok())
{
Some(count) => (0..count)
.map(|i| {
dnf_package(
&format!("pkg{i:04}"),
"1.0-1.fc40",
"x86_64",
"fedora",
1024,
)
})
.collect(),
None => vec![
dnf_package("bash", "5.2.26-1.fc40", "x86_64", "fedora", 7340032),
dnf_package("htop", "3.3.0-1.fc40", "x86_64", "fedora", 245760),
dnf_package("nginx", "1.24.0-7.fc40", "x86_64", "fedora", 1572864),
dnf_package("vim-enhanced", "9.1.0-1.fc40", "x86_64", "updates", 3145728),
],
};
json!({"reply":[[packages]],"id": id})
}
"install" | "remove" | "upgrade" => json!({"reply":[[]],"id": id}),
"resolve" => json!({"reply":[[fake_resolve_items(), 0]],"id": id}),
"do_transaction" => json!({"reply":[[]],"id": id}),
other => json!({"error":[
"org.freedesktop.DBus.Error.UnknownMethod",
[format!("no fake for {other}")]],"id": id}),
}
}
fn dnf_repo(id: &str, name: &str, enabled: bool) -> Value {
json!({
"id": {"t":"s","v":id},
"name": {"t":"s","v":name},
"enabled": {"t":"b","v":enabled},
})
}
fn fake_resolve_items() -> Value {
fn installed(name: &str) -> Value {
json!([
"Package",
"Install",
"User",
{},
dnf_package(name, "1.0-1.fc40", "x86_64", "fedora", 1024),
])
}
fn removed(name: &str) -> Value {
json!([
"Package",
"Remove",
"Dependency",
{},
dnf_package(name, "1.0-1.fc40", "x86_64", "@System", 1024),
])
}
match std::env::var("FEZ_FAKE_PLAN").as_deref() {
Ok("protected") => json!([removed("glibc")]),
Ok("cascade") => {
let items: Vec<Value> = (0..21).map(|i| removed(&format!("pkg{i}"))).collect();
Value::Array(items)
}
Ok("install") | Err(_) => json!([installed("htop")]),
Ok(_) => json!([removed("htop")]),
}
}
const NM_MGR_PATH: &str = "/org/freedesktop/NetworkManager";
fn nm_reply(path: &str, method: &str, id: &Value) -> Value {
if path == NM_MGR_PATH {
return match method {
"GetDevices" => json!({"reply":[[[
format!("{NM_MGR_PATH}/Devices/1"),
format!("{NM_MGR_PATH}/Devices/2"),
format!("{NM_MGR_PATH}/Devices/3"),
format!("{NM_MGR_PATH}/Devices/9"),
]]],"id":id}),
other => nm_unknown(other, id),
};
}
if let Some(n) = path.strip_prefix(&format!("{NM_MGR_PATH}/Devices/")) {
return match method {
"GetAll" => nm_device_props(n, id),
other => nm_unknown(other, id),
};
}
if path.starts_with(&format!("{NM_MGR_PATH}/IP4Config/")) {
return match method {
"GetAll" => json!({"reply":[[{
"AddressData": {"t":"aa{sv}","v":[
{"address":{"t":"s","v":"192.168.10.20"},"prefix":{"t":"u","v":24}}
]},
"Gateway": {"t":"s","v":"192.168.10.1"},
"NameserverData": {"t":"aa{sv}","v":[
{"address":{"t":"s","v":"192.168.10.1"}},
{"address":{"t":"s","v":"1.1.1.1"}}
]},
"Domains": {"t":"as","v":["lan"]},
}]],"id":id}),
other => nm_unknown(other, id),
};
}
if path.starts_with(&format!("{NM_MGR_PATH}/IP6Config/")) {
return match method {
"GetAll" => json!({"reply":[[{
"AddressData": {"t":"aa{sv}","v":[
{"address":{"t":"s","v":"fd00::20"},"prefix":{"t":"u","v":64}}
]},
"Gateway": {"t":"s","v":"fd00::1"},
}]],"id":id}),
other => nm_unknown(other, id),
};
}
if path.starts_with(&format!("{NM_MGR_PATH}/ActiveConnection/")) {
return match method {
"GetAll" => json!({"reply":[[{
"Id": {"t":"s","v":"enp1s0"},
"Type": {"t":"s","v":"802-3-ethernet"},
"Default": {"t":"b","v":true},
}]],"id":id}),
other => nm_unknown(other, id),
};
}
if path.starts_with(&format!("{NM_MGR_PATH}/DHCP4Config/")) {
return match method {
"GetAll" => json!({"reply":[[{
"Options": {"t":"a{sv}","v":{
"routers": {"t":"s","v":"192.168.10.1"},
"ip_address": {"t":"s","v":"192.168.10.20"},
}},
}]],"id":id}),
other => nm_unknown(other, id),
};
}
nm_unknown(method, id)
}
fn nm_device_props(n: &str, id: &Value) -> Value {
let nm = NM_MGR_PATH;
let (iface, dtype, state, managed, ip4, ip6, active, dhcp4) = match n {
"1" => (
"enp1s0",
1u32,
100u32,
true,
format!("{nm}/IP4Config/1"),
format!("{nm}/IP6Config/1"),
format!("{nm}/ActiveConnection/1"),
format!("{nm}/DHCP4Config/1"),
),
"2" => (
"enp2s0",
1u32,
20u32,
true,
"/".to_string(),
"/".to_string(),
"/".to_string(),
"/".to_string(),
),
"3" => (
"lo",
32u32,
10u32,
false,
"/".to_string(),
"/".to_string(),
"/".to_string(),
"/".to_string(),
),
_ => (
"veth0",
20u32,
10u32,
false,
"/".to_string(),
"/".to_string(),
"/".to_string(),
"/".to_string(),
),
};
json!({"reply":[[{
"Interface": {"t":"s","v":iface},
"DeviceType": {"t":"u","v":dtype},
"State": {"t":"u","v":state},
"Managed": {"t":"b","v":managed},
"HwAddress": {"t":"s","v":"52:54:00:12:34:56"},
"Mtu": {"t":"u","v":1500},
"Ip4Config": {"t":"o","v":ip4},
"Ip6Config": {"t":"o","v":ip6},
"ActiveConnection": {"t":"o","v":active},
"Dhcp4Config": {"t":"o","v":dhcp4},
}]],"id":id})
}
fn nm_unknown(method: &str, id: &Value) -> Value {
json!({"error":[
"org.freedesktop.DBus.Error.UnknownMethod",
[format!("no NM fake for {method}")]],"id": id})
}
const FW_PATH: &str = "/org/fedoraproject/FirewallD1";
const FW_CONFIG_PATH: &str = "/org/fedoraproject/FirewallD1/config";
const FW_IFACE: &str = "org.fedoraproject.FirewallD1";
const FW_ZONE_IFACE: &str = "org.fedoraproject.FirewallD1.zone";
const FW_CONFIG_IFACE: &str = "org.fedoraproject.FirewallD1.config";
const FW_CONFIG_ZONE_IFACE: &str = "org.fedoraproject.FirewallD1.config.zone";
fn fw_reply(
path: &str,
iface: &str,
method: &str,
args: &[Value],
on_privileged: bool,
id: &Value,
) -> Value {
if std::env::var_os("FEZ_FAKE_NO_FIREWALLD").is_some() {
return json!({"error":[
"org.freedesktop.DBus.Error.ServiceUnknown",
["The name org.fedoraproject.FirewallD1 was not provided by any .service files"]
],"id": id});
}
if path == FW_CONFIG_PATH && std::env::var_os("FEZ_FAKE_CONFIG_UNKNOWN_METHOD").is_some() {
return fw_unknown(method, id);
}
if path.starts_with(FW_CONFIG_PATH) && std::env::var_os("FEZ_FAKE_CONFIG_INFO_DENIED").is_some()
{
return fw_config_info_denied(id);
}
if path.starts_with(&format!("{FW_CONFIG_PATH}/zone/")) {
if !on_privileged {
return fw_access_denied(id);
}
return match (iface, method) {
(FW_CONFIG_ZONE_IFACE, "getServices") => {
json!({"reply":[[["ssh", "dhcpv6-client"]]],"id": id})
}
(FW_CONFIG_ZONE_IFACE, "getPorts") => json!({"reply":[[[]]],"id": id}),
(FW_CONFIG_ZONE_IFACE, "getMasquerade") => json!({"reply":[[false]],"id": id}),
(_, other) => fw_unknown(other, id),
};
}
if path == FW_CONFIG_PATH {
if !on_privileged {
return fw_access_denied(id);
}
return match (iface, method) {
(FW_CONFIG_IFACE, "getZoneByName") => {
json!({"reply":[[format!("{FW_CONFIG_PATH}/zone/0")]],"id": id})
}
(_, other) => fw_unknown(other, id),
};
}
let zone = args.first().and_then(Value::as_str).unwrap_or("");
match (iface, method) {
(FW_IFACE, "getDefaultZone") => json!({"reply":[["public"]],"id": id}),
(FW_IFACE, "listServices") => json!({"reply":[[[
"ssh", "http", "https", "cockpit", "dhcpv6-client"
]]],"id": id}),
(FW_IFACE, "queryPanicMode") => {
let on = std::env::var_os("FEZ_FAKE_PANIC").is_some();
json!({"reply":[[on]],"id": id})
}
(FW_ZONE_IFACE, "getZones") => {
json!({"reply":[[["public", "internal", "drop"]]],"id": id})
}
(FW_ZONE_IFACE, "getServices") => json!({"reply":[[["ssh", "dhcpv6-client"]]],"id": id}),
(FW_ZONE_IFACE, "getPorts") => {
let removed = std::env::var_os("FEZ_FAKE_PORT_REMOVED").is_some();
if zone == "public" && !removed {
json!({"reply":[[[["9090", "tcp"]]]],"id": id})
} else {
json!({"reply":[[[]]],"id": id})
}
}
(FW_ZONE_IFACE, "getInterfaces") => {
if zone == "public" {
json!({"reply":[[["enp1s0"]]],"id": id})
} else {
json!({"reply":[[[]]],"id": id})
}
}
(FW_ZONE_IFACE, "getSources") => json!({"reply":[[[]]],"id": id}),
(FW_ZONE_IFACE, "getMasquerade") => {
if std::env::var_os("FEZ_FAKE_NO_MASQUERADE").is_some() {
return fw_unknown("getMasquerade", id);
}
if zone == "public" {
json!({"reply":[[true]],"id": id})
} else {
json!({"reply":[[false]],"id": id})
}
}
(
FW_ZONE_IFACE,
"addService" | "removeService" | "addPort" | "removePort" | "addMasquerade"
| "removeMasquerade",
) => {
json!({"reply":[[zone]],"id": id})
}
(FW_IFACE, "setDefaultZone" | "reload" | "runtimeToPermanent")
| (FW_IFACE, "enablePanicMode" | "disablePanicMode") => json!({"reply":[[]],"id": id}),
(_, other) => fw_unknown(other, id),
}
}
fn fw_unknown(method: &str, id: &Value) -> Value {
json!({"error":[
"org.freedesktop.DBus.Error.UnknownMethod",
[format!("no firewalld fake for {method}")]],"id": id})
}
fn fw_access_denied(id: &Value) -> Value {
json!({"error":[
"org.freedesktop.DBus.Error.AccessDenied",
["permanent config read requires authorization (PK_ACTION_CONFIG)"]],"id": id})
}
fn fw_config_info_denied(id: &Value) -> Value {
json!({"error":[
"org.fedoraproject.FirewallD1.NotAuthorizedException",
["Not Authorized(polkit): org.fedoraproject.FirewallD1.config.info"]],"id": id})
}
fn fake_bridges() -> Vec<(String, bool)> {
let raw = match std::env::var("FEZ_FAKE_BRIDGES") {
Ok(v) => v,
Err(_) => return vec![("sudo".to_string(), true)],
};
raw.split(',')
.filter(|s| !s.is_empty())
.map(|entry| {
let (name, outcome) = entry.split_once(':').unwrap_or((entry, "ok"));
(name.to_string(), outcome == "ok")
})
.collect()
}
fn send_control(out: &mut impl Write, v: &Value) {
let mut payload = serde_json::to_vec(v).unwrap();
payload.push(b'\n');
write_frame(
out,
&Frame {
channel: String::new(),
payload,
},
)
.unwrap();
}
fn send_data(out: &mut impl Write, channel: &str, v: &Value) {
let mut payload = serde_json::to_vec(v).unwrap();
payload.push(b'\n');
write_frame(out, &Frame::new(channel, payload)).unwrap();
}
const PK_PATH: &str = "/org/freedesktop/PackageKit";
const PK_TX_PATH: &str = "/org/freedesktop/PackageKit/Transaction/1";
const PK_TX_IFACE: &str = "org.freedesktop.PackageKit.Transaction";
fn send_signal(out: &mut impl Write, channel: &str, member: &str, args: Value) {
send_data(
out,
channel,
&json!({ "signal": [PK_TX_PATH, PK_TX_IFACE, member, args] }),
);
}
fn pk_emit(out: &mut impl Write, channel: &str, method: &str) {
let installed = [
(
1u64,
"bash;5.2.26-1.fc44;x86_64;installed",
"GNU Bourne-Again SHell",
),
(
1u64,
"htop;3.4.1-3.fc44;x86_64;installed",
"Interactive process viewer",
),
];
let nginx = "nginx;1.27.0-1.fc44;x86_64;fedora";
match method {
"GetPackages" => {
for (info, pid, summ) in installed {
send_signal(out, channel, "Package", json!([info, pid, summ]));
}
}
"GetUpdates" => {
send_signal(
out,
channel,
"Package",
json!([
7,
"htop;3.4.2-1.fc44;x86_64;updates",
"Interactive process viewer"
]),
);
}
"SearchNames" => {
send_signal(
out,
channel,
"Package",
json!([2, nginx, "High performance web server"]),
);
}
"GetRepoList" => {
send_signal(
out,
channel,
"RepoDetail",
json!(["fedora", "Fedora 44", true]),
);
send_signal(
out,
channel,
"RepoDetail",
json!(["updates", "Fedora 44 updates", true]),
);
send_signal(out, channel, "RepoDetail", json!(["crb", "CRB", false]));
}
"Resolve" => {
send_signal(
out,
channel,
"Package",
json!([2, nginx, "High performance web server"]),
);
}
"InstallPackages" | "UpdatePackages" => {
send_signal(
out,
channel,
"Package",
json!([8, nginx, "High performance web server"]),
);
send_signal(
out,
channel,
"Package",
json!([
8,
"nginx-core;1.27.0-1.fc44;x86_64;fedora",
"nginx core files"
]),
);
}
"RemovePackages" => {
send_signal(
out,
channel,
"Package",
json!([
9,
"htop;3.4.1-3.fc44;x86_64;installed",
"Interactive process viewer"
]),
);
if std::env::var("FEZ_FAKE_PK_PLAN").as_deref() == Ok("protected") {
send_signal(
out,
channel,
"Package",
json!([
9,
"systemd;255-1.fc44;x86_64;installed",
"System and Service Manager"
]),
);
}
}
_ => {}
}
if std::env::var("FEZ_FAKE_PK_ERROR").as_deref() == Ok("notauth") {
send_signal(
out,
channel,
"ErrorCode",
json!([6, "not authorized to perform operation"]),
);
send_signal(out, channel, "Finished", json!([4, 10]));
return;
}
send_signal(out, channel, "Finished", json!([1, 20])); }
fn main() -> io::Result<()> {
let mut stdin = io::stdin().lock();
let mut stdout = io::stdout().lock();
let bridges = fake_bridges();
let mut escalated = false;
let mut privileged_channels: std::collections::HashSet<String> =
std::collections::HashSet::new();
send_control(&mut stdout, &json!({"command":"init","version":1}));
while let Some(frame) = read_frame(&mut stdin)? {
if frame.channel.is_empty() {
let ctrl: Value = serde_json::from_slice(&frame.payload).unwrap_or(Value::Null);
let command = ctrl.get("command").and_then(Value::as_str);
if let Some("init") = command {
let requests_escalation = match ctrl.get("superuser") {
None | Some(Value::Null) => false,
Some(Value::String(s)) => s != "none",
Some(_) => true,
};
if requests_escalation {
send_control(&mut stdout, &json!({"command":"superuser-init-done"}));
}
continue;
}
if let Some("open") = command {
let channel = ctrl
.get("channel")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let payload = ctrl.get("payload").and_then(Value::as_str).unwrap_or("");
let open_name = ctrl.get("name").and_then(Value::as_str).unwrap_or("");
if std::env::var_os("FEZ_FAKE_FIREWALLD_UNREACHABLE").is_some()
&& open_name == "org.fedoraproject.FirewallD1"
{
send_control(
&mut stdout,
&json!({"command":"close","channel":channel,"problem":"not-found"}),
);
continue;
}
let privileged = ctrl.get("superuser").and_then(Value::as_str) == Some("require");
let force_deny = std::env::var_os("FEZ_FAKE_DENY_PRIVILEGED").is_some();
let deny_privileged = !escalated || force_deny;
if privileged && deny_privileged {
send_control(
&mut stdout,
&json!({"command":"close","channel":channel,"problem":"access-denied"}),
);
continue;
}
if privileged {
privileged_channels.insert(channel.clone());
}
send_control(&mut stdout, &json!({"command":"ready","channel":channel}));
if payload == "stream" {
let mut blob = serde_json::to_vec(&json!({
"__REALTIME_TIMESTAMP":"1700000000000000","PRIORITY":"6",
"SYSLOG_IDENTIFIER":"sshd","MESSAGE":"Server listening on port 22.","_PID":"1001"
})).unwrap();
blob.push(b'\n');
blob.extend_from_slice(
&serde_json::to_vec(&json!({
"__REALTIME_TIMESTAMP":"1700000001000000","PRIORITY":"6",
"SYSLOG_IDENTIFIER":"sshd","MESSAGE":"Accepted publickey for fedora","_PID":"1002"
}))
.unwrap(),
);
blob.push(b'\n');
write_frame(&mut stdout, &Frame::new(&channel, blob))?;
send_control(&mut stdout, &json!({"command":"done","channel":channel}));
send_control(&mut stdout, &json!({"command":"close","channel":channel}));
}
}
} else {
let msg: Value = serde_json::from_slice(&frame.payload).unwrap_or(Value::Null);
if let Some(call) = msg.get("call").and_then(Value::as_array) {
let id = msg.get("id").cloned().unwrap_or(json!("0"));
let path = call.first().and_then(Value::as_str).unwrap_or("");
let iface = call.get(1).and_then(Value::as_str).unwrap_or("");
let method = call.get(2).and_then(Value::as_str).unwrap_or("");
let args = call
.get(3)
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let dnf_options_method = matches!(
method,
"open_session"
| "list"
| "install"
| "remove"
| "upgrade"
| "resolve"
| "do_transaction"
);
let reply = if path.starts_with(FW_PATH) {
let on_privileged = privileged_channels.contains(&frame.channel);
fw_reply(path, iface, method, &args, on_privileged, &id)
} else if path.starts_with(NM_MGR_PATH) {
nm_reply(path, method, &id)
} else if path == PK_PATH && method == "CreateTransaction" {
if std::env::var_os("FEZ_FAKE_NO_PACKAGEKIT").is_some() {
json!({"error":[
"org.freedesktop.DBus.Error.ServiceUnknown",
["The name org.freedesktop.PackageKit was not provided by any .service files"]
],"id": id})
} else {
json!({"reply":[[PK_TX_PATH]],"id":id})
}
} else if path == PK_TX_PATH {
pk_emit(&mut stdout, &frame.channel, method);
continue;
} else if dnf_options_method {
if let Some(err) = reject_unwrapped_options(&args, &id) {
send_data(&mut stdout, &frame.channel, &err);
continue;
}
dnf_reply(method, iface, &id)
} else {
match method {
"Get" => {
let names: Vec<Value> = bridges.iter().map(|(n, _)| json!(n)).collect();
json!({"reply":[[{"t":"as","v":names}]],"id":id})
}
"Start" => {
let name = args.first().and_then(Value::as_str).unwrap_or("");
match bridges.iter().find(|(n, _)| n == name) {
Some((_, true)) => {
escalated = true;
json!({"reply":[[]],"id":id})
}
_ => json!({"error":[
"cockpit.Superuser.Error",
[format!("mechanism {name:?} cannot start")]],"id":id}),
}
}
"ListUnits" => json!({"reply":[[[
["sshd.service","OpenSSH server daemon","loaded","active","running","",
"/org/freedesktop/systemd1/unit/sshd_2eservice",0,"","/"],
["chronyd.service","NTP client/server","loaded","inactive","dead","",
"/org/freedesktop/systemd1/unit/chronyd_2eservice",0,"","/"]
]]],"id":id}),
"GetUnit" | "LoadUnit" => {
json!({"reply":[["/org/freedesktop/systemd1/unit/sshd_2eservice"]],"id":id})
}
"GetAll" => json!({"reply":[[{
"Id":{"t":"s","v":"sshd.service"},
"Description":{"t":"s","v":"OpenSSH server daemon"},
"LoadState":{"t":"s","v":"loaded"},
"ActiveState":{"t":"s","v":"active"},
"SubState":{"t":"s","v":"running"},
"UnitFileState":{"t":"s","v":"enabled"}
}]],"id":id}),
"StartUnit" | "StopUnit" | "RestartUnit" | "ReloadUnit" => {
json!({"reply":[["/org/freedesktop/systemd1/job/42"]],"id":id})
}
"Reload" => json!({"reply":[[]],"id":id}),
"EnableUnitFiles" => json!({"reply":[[
true,
[["symlink",
"/etc/systemd/system/multi-user.target.wants/chronyd.service",
"/usr/lib/systemd/system/chronyd.service"]]
]],"id":id}),
"DisableUnitFiles" => json!({"reply":[[
[["unlink",
"/etc/systemd/system/multi-user.target.wants/chronyd.service",
""]]
]],"id":id}),
"close_session" => json!({"reply":[[true]],"id":id}),
other => json!({"error":[
"org.freedesktop.DBus.Error.UnknownMethod",
[format!("no fake for {other}")]],"id":id}),
}
};
send_data(&mut stdout, &frame.channel, &reply);
}
}
}
Ok(())
}