use std::collections::BTreeMap;
use act_types::{Capabilities, CapabilityRequest, SocketProtocol};
use crate::Decision;
use crate::effective::effective_sockets;
use crate::grant::{CapabilityGrant, PolicyError, SocketsConfig, SocketsRule};
use crate::net::{NetworkCheck, rule_matches};
use crate::provider::{CapabilityProvider, CompiledCeiling, ResourceOp};
pub struct SocketsProvider;
#[async_trait::async_trait]
impl CapabilityProvider for SocketsProvider {
async fn resolve(
&self,
cap_id: &str,
declared: Option<&[serde_json::Value]>,
grant: &CapabilityGrant,
) -> Result<Box<dyn CompiledCeiling>, PolicyError> {
let declared = declared.unwrap_or(&[]);
let user = sockets_config_from_grant(grant)?;
let decl_rules = parse_sockets_rules(declared)?;
let caps = caps_from_declared(cap_id, declared);
let eff = effective_sockets(&user, &caps);
let is_declared = eff.declared;
#[cfg(feature = "host")]
let (config, decl_rules) = {
let mut config = eff.config;
config.allow = pin_hostnames(config.allow).await;
config.deny = pin_hostnames(config.deny).await;
(config, pin_hostnames(decl_rules).await)
};
#[cfg(not(feature = "host"))]
let (config, decl_rules) = (eff.config, decl_rules);
Ok(Box::new(SocketsCeiling {
config,
decl_rules,
is_declared,
}))
}
}
#[cfg(feature = "host")]
async fn pin_hostnames(rules: Vec<SocketsRule>) -> Vec<SocketsRule> {
use std::net::IpAddr;
let mut out = Vec::new();
for rule in rules {
if let Some(host) = rule.net.host.as_deref()
&& host != "*"
&& host.parse::<IpAddr>().is_err()
{
if let Ok(addrs) = tokio::net::lookup_host((host, 0u16)).await {
for addr in addrs {
let mut synth = rule.clone();
synth.net.host = None;
synth.net.cidr = Some(match addr.ip() {
IpAddr::V4(v4) => format!("{v4}/32"),
IpAddr::V6(v6) => format!("{v6}/128"),
});
out.push(synth);
}
} else {
tracing::warn!(
host = %host,
"wasi:sockets rule host did not resolve; rule has no effect"
);
}
}
out.push(rule);
}
out
}
struct SocketsCeiling {
config: SocketsConfig,
decl_rules: Vec<SocketsRule>,
is_declared: bool,
}
impl CompiledCeiling for SocketsCeiling {
fn classify(&self, op: &ResourceOp) -> Decision {
let (host, port) = parse_host_port(&op.key);
let check = NetworkCheck::new(host, port);
let protocol = op
.attrs
.get("protocol")
.and_then(|v| v.as_str())
.and_then(|s| match s {
"tcp" => Some(SocketProtocol::Tcp),
"udp" => Some(SocketProtocol::Udp),
_ => None,
});
match self.config.mode {
crate::grant::PolicyMode::Deny => Decision::Deny,
crate::grant::PolicyMode::Open => Decision::Allow,
crate::grant::PolicyMode::Ask => {
if self
.config
.deny
.iter()
.any(|r| rule_matches(&r.net, &check))
{
return Decision::Deny;
}
let in_ceiling = self.config.allow.iter().any(|eff_rule| {
rule_matches(&eff_rule.net, &check)
&& decl_allows_protocol(&self.decl_rules, &check, protocol.as_ref())
});
if in_ceiling {
Decision::Ask
} else {
Decision::Deny
}
}
crate::grant::PolicyMode::Allowlist => {
if self
.config
.deny
.iter()
.any(|r| rule_matches(&r.net, &check))
{
return Decision::Deny;
}
if self.config.allow.iter().any(|eff_rule| {
rule_matches(&eff_rule.net, &check)
&& decl_allows_protocol(&self.decl_rules, &check, protocol.as_ref())
}) {
Decision::Allow
} else {
Decision::Deny
}
}
}
}
fn declared(&self) -> bool {
self.is_declared
}
fn effective_mode(&self) -> crate::grant::PolicyMode {
self.config.mode
}
}
fn decl_allows_protocol(
decl_rules: &[SocketsRule],
check: &NetworkCheck,
protocol: Option<&SocketProtocol>,
) -> bool {
if decl_rules.is_empty() {
return true;
}
decl_rules.iter().any(|r| {
if !rule_matches(&r.net, check) {
return false;
}
if let Some(allowed_protocols) = &r.protocols
&& let Some(req_protocol) = protocol
&& !allowed_protocols.contains(req_protocol)
{
return false;
}
true
})
}
fn parse_host_port(key: &str) -> (&str, u16) {
if key.starts_with('[')
&& let Some(bracket_end) = key.find(']')
{
let host = &key[..=bracket_end];
if let Some(port_str) = key.get(bracket_end + 2..)
&& let Ok(port) = port_str.parse::<u16>()
{
return (host, port);
}
return (host, 0);
}
if let Some(colon_pos) = key.rfind(':') {
let port_str = &key[colon_pos + 1..];
if let Ok(port) = port_str.parse::<u16>() {
return (&key[..colon_pos], port);
}
}
(key, 0)
}
fn sockets_config_from_grant(grant: &CapabilityGrant) -> Result<SocketsConfig, PolicyError> {
let allow = parse_sockets_rules(&grant.allow)?;
let deny = parse_sockets_rules(&grant.deny)?;
Ok(SocketsConfig {
mode: grant.mode,
allow,
deny,
})
}
fn parse_sockets_rules(cs: &[serde_json::Value]) -> Result<Vec<SocketsRule>, PolicyError> {
cs.iter()
.map(|c| {
serde_json::from_value::<SocketsRule>(c.clone()).map_err(|e| PolicyError::Constraint {
cap: "wasi:sockets",
source: e,
})
})
.collect()
}
fn caps_from_declared(cap_id: &str, declared: &[serde_json::Value]) -> Capabilities {
if declared.is_empty() {
return Capabilities::default();
}
let req = CapabilityRequest {
constraints: declared.to_vec(),
..Default::default()
};
Capabilities(BTreeMap::from([(cap_id.to_string(), req)]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Decision;
use crate::grant::{CapabilityGrant, PolicyMode};
use crate::provider::{CapabilityProvider, ResourceOp};
use serde_json::json;
#[tokio::test]
async fn sockets_provider_matches_host_port_protocol() {
let p = SocketsProvider;
let declared = vec![json!({
"host": "198.51.100.7",
"ports": [5900],
"protocols": ["tcp"]
})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![json!({"host": "198.51.100.7", "ports": [5900]})],
deny: vec![],
};
let c = p
.resolve("wasi:sockets", Some(&declared), &grant)
.await
.unwrap();
let ok_op = ResourceOp {
cap_id: "wasi:sockets".into(),
key: "198.51.100.7:5900".into(),
action: String::new(),
attrs: json!({"protocol": "tcp"}),
};
assert_eq!(c.classify(&ok_op), Decision::Allow);
let bad_proto_op = ResourceOp {
cap_id: "wasi:sockets".into(),
key: "198.51.100.7:5900".into(),
action: String::new(),
attrs: json!({"protocol": "udp"}),
};
assert_eq!(c.classify(&bad_proto_op), Decision::Deny);
}
#[cfg(feature = "host")]
#[tokio::test]
async fn sockets_provider_pins_hostname_to_resolved_ip() {
let p = SocketsProvider;
let declared = vec![json!({"host":"localhost","ports":[5900],"protocols":["tcp"]})];
let grant = CapabilityGrant {
mode: PolicyMode::Allowlist,
allow: vec![json!({"host":"localhost","ports":[5900]})],
deny: vec![],
};
let c = p
.resolve("wasi:sockets", Some(&declared), &grant)
.await
.unwrap();
let op = ResourceOp {
cap_id: "wasi:sockets".into(),
key: "127.0.0.1:5900".into(),
action: String::new(),
attrs: json!({"protocol": "tcp"}),
};
assert_eq!(c.classify(&op), Decision::Allow);
}
#[tokio::test]
async fn sockets_provider_undeclared_denies_all() {
let p = SocketsProvider;
let grant = CapabilityGrant {
mode: PolicyMode::Open,
allow: vec![],
deny: vec![],
};
let c = p.resolve("wasi:sockets", None, &grant).await.unwrap();
let op = ResourceOp {
cap_id: "wasi:sockets".into(),
key: "host.example.com:5900".into(),
action: String::new(),
attrs: json!({"protocol": "tcp"}),
};
assert_eq!(c.classify(&op), Decision::Deny);
assert!(!c.declared());
}
}