feat(manifest): add allow-listed per-netns sysctls primitive
Routing apps (a rootless VPN exit) need packet forwarding in their own network namespace, but /proc/sys is read-only inside a rootless container, so it can only be set at create time. Add `app.sysctls`, allow-listed to net.ipv4.ip_forward / net.ipv6.conf.all.forwarding with values "0"/"1", and rejected under host networking where it would change the host. Rendered on all three create paths: podman CLI --sysctl, the libpod spec `sysctl` map, and Quadlet `Sysctl=`. Absent by default and not serialized when empty, so existing manifests and units are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,8 @@ pub struct QuadletUnit {
|
||||
/// for rotation-drift detection.
|
||||
pub labels: Vec<(String, String)>,
|
||||
pub devices: Vec<String>,
|
||||
/// Namespaced sysctls (`Sysctl=k=v`), already allow-listed by the manifest.
|
||||
pub sysctls: Vec<(String, String)>,
|
||||
pub add_hosts: Vec<(String, String)>,
|
||||
pub network_aliases: Vec<String>,
|
||||
pub entrypoint: Option<Vec<String>>,
|
||||
@@ -307,6 +309,9 @@ impl QuadletUnit {
|
||||
for dev in &self.devices {
|
||||
let _ = writeln!(s, "AddDevice={dev}");
|
||||
}
|
||||
for (k, v) in &self.sysctls {
|
||||
let _ = writeln!(s, "Sysctl={k}={v}");
|
||||
}
|
||||
for (name, ip) in &self.add_hosts {
|
||||
let _ = writeln!(s, "AddHost={name}:{ip}");
|
||||
}
|
||||
@@ -521,6 +526,11 @@ impl QuadletUnit {
|
||||
})
|
||||
.collect(),
|
||||
devices: app.devices.clone(),
|
||||
sysctls: app
|
||||
.sysctls
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())],
|
||||
// Container always answers to its own name; manifest extras add the
|
||||
// short hostnames peers bake in (e.g. indeedhub api/minio/relay).
|
||||
@@ -1487,6 +1497,7 @@ app:
|
||||
"RELAY_NAME=Archipelago Nostr Relay".into(),
|
||||
],
|
||||
devices: vec!["/dev/kvm".into()],
|
||||
sysctls: vec![("net.ipv4.ip_forward".into(), "1".into())],
|
||||
add_hosts: vec![("host.archipelago".into(), "10.89.0.1".into())],
|
||||
entrypoint: Some(vec!["/usr/local/bin/bitcoind".into()]),
|
||||
command: vec!["-server=1".into(), "-rpcbind=0.0.0.0".into()],
|
||||
@@ -1503,6 +1514,7 @@ app:
|
||||
assert!(s.contains("Environment=BITCOIN_RPC_PASS=secret"));
|
||||
assert!(s.contains("Environment=\"RELAY_NAME=Archipelago Nostr Relay\""));
|
||||
assert!(s.contains("AddDevice=/dev/kvm"));
|
||||
assert!(s.contains("Sysctl=net.ipv4.ip_forward=1"));
|
||||
assert!(s.contains("AddHost=host.archipelago:10.89.0.1"));
|
||||
assert!(s.contains("ReadOnly=true"));
|
||||
assert!(s.contains("NoNewPrivileges=true"));
|
||||
@@ -1524,6 +1536,7 @@ app:
|
||||
assert!(!s.contains("PublishPort="));
|
||||
assert!(!s.contains("Environment="));
|
||||
assert!(!s.contains("AddDevice="));
|
||||
assert!(!s.contains("Sysctl="));
|
||||
assert!(!s.contains("AddHost="));
|
||||
assert!(!s.contains("ReadOnly="));
|
||||
assert!(!s.contains("NoNewPrivileges="));
|
||||
@@ -1621,6 +1634,31 @@ app:
|
||||
assert!(!s.contains("Network=host"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_manifest_renders_namespaced_sysctls() {
|
||||
let yaml = r#"
|
||||
app:
|
||||
id: vpn-exit
|
||||
name: VPN Exit
|
||||
version: 1.0.0
|
||||
container:
|
||||
image: test/vpn:1.0.0
|
||||
network: pasta
|
||||
devices: [/dev/net/tun]
|
||||
sysctls:
|
||||
net.ipv4.ip_forward: "1"
|
||||
security:
|
||||
capabilities: [NET_ADMIN, NET_RAW]
|
||||
"#;
|
||||
let m = AppManifest::parse(yaml).expect("manifest must parse");
|
||||
let s = QuadletUnit::from_manifest(&m, "vpn-exit").render();
|
||||
|
||||
assert!(s.contains("Network=pasta"));
|
||||
assert!(s.contains("AddDevice=/dev/net/tun"));
|
||||
assert!(s.contains("Sysctl=net.ipv4.ip_forward=1"));
|
||||
assert!(s.contains("AddCapability=NET_ADMIN"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portainer_catalog_network_repairs_same_node_routing_without_exposing_backend() {
|
||||
let manifest = AppManifest::parse(include_str!("../../../../apps/portainer/manifest.yml"))
|
||||
|
||||
Reference in New Issue
Block a user