diff --git a/cli/src/main.rs b/cli/src/main.rs index 7df98cae..6a625fb0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -312,6 +312,7 @@ async fn create_execution_context( if let Some(config_path) = args.config_path() { let config = FastlyConfig::from_file(config_path)?; + let acls = config.acls(); let backends = config.backends(); let device_detection = config.device_detection(); let geolocation = config.geolocation(); @@ -321,6 +322,7 @@ async fn create_execution_context( let backend_names = itertools::join(backends.keys(), ", "); ctx = ctx + .with_acls(acls.clone()) .with_backends(backends.clone()) .with_device_detection(device_detection.clone()) .with_geolocation(geolocation.clone()) diff --git a/cli/tests/integration/acl.rs b/cli/tests/integration/acl.rs new file mode 100644 index 00000000..ac8cd305 --- /dev/null +++ b/cli/tests/integration/acl.rs @@ -0,0 +1,113 @@ +use crate::{common::Test, common::TestResult, viceroy_test}; +use hyper::{body::to_bytes, StatusCode}; +use viceroy_lib::config::FastlyConfig; +use viceroy_lib::error::{AclConfigError, FastlyConfigError}; + +viceroy_test!(acl_works, |is_component| { + const FASTLY_TOML: &str = r#" + name = "acl" + description = "acl test" + authors = ["Test User "] + language = "rust" + [local_server] + acls.my-acl-1 = "../test-fixtures/data/my-acl-1.json" + acls.my-acl-2 = {file = "../test-fixtures/data/my-acl-2.json"} + "#; + + let resp = Test::using_fixture("acl.wasm") + .adapt_component(is_component) + .using_fastly_toml(FASTLY_TOML)? + .log_stderr() + .log_stdout() + .against_empty() + .await?; + + assert_eq!(resp.status(), StatusCode::OK); + assert!(to_bytes(resp.into_body()) + .await + .expect("can read body") + .to_vec() + .is_empty()); + + Ok(()) +}); + +fn bad_config_test(local_server_fragment: &str) -> Result { + let toml = format!( + r#" + name = "acl" + description = "acl test" + authors = ["Test User "] + language = "rust" + [local_server] + {} + "#, + local_server_fragment + ); + + toml.parse::() +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_invalid_path() -> TestResult { + const TOML_FRAGMENT: &str = "acls.bad = 1"; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidAclDefinition { + err: AclConfigError::InvalidType, + .. + }) => (), + Err(_) => panic!( + "expected a FastlyConfigError::InvalidAclDefinition with AclConfigError::InvalidType" + ), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_missing_key() -> TestResult { + const TOML_FRAGMENT: &str = "acls.bad = { \"other\" = true }"; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidAclDefinition { + err: AclConfigError::MissingFile, + .. + }) => (), + Err(_) => panic!( + "expected a FastlyConfigError::InvalidAclDefinition with AclConfigError::MissingFile" + ), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_missing_file() -> TestResult { + const TOML_FRAGMENT: &str = "acls.bad = \"/does/not/exist\""; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidAclDefinition { + err: AclConfigError::IoError(_), + .. + }) => (), + Err(_) => panic!( + "expected a FastlyConfigError::InvalidAclDefinition with AclConfigError::IoError" + ), + _ => panic!("Expected an error"), + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn bad_config_invalid_json() -> TestResult { + const TOML_FRAGMENT: &str = "acls.bad = \"../Cargo.toml\""; + match bad_config_test(TOML_FRAGMENT) { + Err(FastlyConfigError::InvalidAclDefinition { + err: AclConfigError::JsonError(_), + .. + }) => (), + Err(_) => panic!( + "expected a FastlyConfigError::InvalidAclDefinition with AclConfigError::JsonError" + ), + _ => panic!("Expected an error"), + } + Ok(()) +} diff --git a/cli/tests/integration/common.rs b/cli/tests/integration/common.rs index f2c5239f..31beabb9 100644 --- a/cli/tests/integration/common.rs +++ b/cli/tests/integration/common.rs @@ -16,7 +16,7 @@ use viceroy_lib::config::UnknownImportBehavior; use viceroy_lib::{ body::Body, config::{ - DeviceDetection, Dictionaries, FastlyConfig, Geolocation, ObjectStores, SecretStores, + Acls, DeviceDetection, Dictionaries, FastlyConfig, Geolocation, ObjectStores, SecretStores, }, ExecuteCtx, ProfilingStrategy, ViceroyService, }; @@ -77,6 +77,7 @@ pub type TestResult = Result<(), Error>; /// A builder for running individual requests through a wasm fixture. pub struct Test { module_path: PathBuf, + acls: Acls, backends: TestBackends, device_detection: DeviceDetection, dictionaries: Dictionaries, @@ -99,6 +100,7 @@ impl Test { Self { module_path, + acls: Acls::new(), backends: TestBackends::new(), device_detection: DeviceDetection::new(), dictionaries: Dictionaries::new(), @@ -121,6 +123,7 @@ impl Test { Self { module_path, + acls: Acls::new(), backends: TestBackends::new(), device_detection: DeviceDetection::new(), dictionaries: Dictionaries::new(), @@ -140,6 +143,7 @@ impl Test { pub fn using_fastly_toml(self, fastly_toml: &str) -> Result { let config = fastly_toml.parse::()?; Ok(Self { + acls: config.acls().to_owned(), backends: TestBackends::from_backend_configs(config.backends()), device_detection: config.device_detection().to_owned(), dictionaries: config.dictionaries().to_owned(), @@ -328,6 +332,7 @@ impl Test { self.unknown_import_behavior, self.adapt_component, )? + .with_acls(self.acls.clone()) .with_backends(self.backends.backend_configs().await) .with_dictionaries(self.dictionaries.clone()) .with_device_detection(self.device_detection.clone()) diff --git a/cli/tests/integration/main.rs b/cli/tests/integration/main.rs index fe798ebe..b1d649cc 100644 --- a/cli/tests/integration/main.rs +++ b/cli/tests/integration/main.rs @@ -1,3 +1,4 @@ +mod acl; mod args; mod async_io; mod body; diff --git a/crates/adapter/src/fastly/core.rs b/crates/adapter/src/fastly/core.rs index 8c45d638..cb1e1c34 100644 --- a/crates/adapter/src/fastly/core.rs +++ b/crates/adapter/src/fastly/core.rs @@ -86,19 +86,20 @@ pub enum HttpKeepaliveMode { NoKeepalive = 1, } -pub type PendingObjectStoreLookupHandle = u32; -pub type PendingObjectStoreInsertHandle = u32; +pub type AclHandle = u32; +pub type AsyncItemHandle = u32; +pub type BodyHandle = u32; +pub type DictionaryHandle = u32; +pub type KVStoreHandle = u32; pub type PendingObjectStoreDeleteHandle = u32; +pub type PendingObjectStoreInsertHandle = u32; pub type PendingObjectStoreListHandle = u32; -pub type BodyHandle = u32; +pub type PendingObjectStoreLookupHandle = u32; pub type PendingRequestHandle = u32; pub type RequestHandle = u32; pub type ResponseHandle = u32; -pub type DictionaryHandle = u32; -pub type KVStoreHandle = u32; -pub type SecretStoreHandle = u32; pub type SecretHandle = u32; -pub type AsyncItemHandle = u32; +pub type SecretStoreHandle = u32; const INVALID_HANDLE: u32 = u32::MAX - 1; @@ -303,6 +304,57 @@ pub struct InspectConfig { pub workspace_len: u32, } +pub mod fastly_acl { + use super::*; + use crate::bindings::fastly::api::acl; + use core::slice; + + #[export_name = "fastly_acl#open"] + pub fn open( + acl_name_ptr: *const u8, + acl_name_len: usize, + acl_handle_out: *mut AclHandle, + ) -> FastlyStatus { + let acl_name = unsafe { slice::from_raw_parts(acl_name_ptr, acl_name_len) }; + match acl::open(acl_name) { + Ok(res) => { + unsafe { + *acl_handle_out = res; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } + + #[export_name = "fastly_acl#lookup"] + pub fn lookup( + acl_handle: acl::AclHandle, + ip_octets: *const u8, + ip_len: usize, + body_handle_out: *mut BodyHandle, + acl_error_out: *mut acl::AclError, + ) -> FastlyStatus { + let ip = unsafe { slice::from_raw_parts(ip_octets, ip_len) }; + match acl::lookup(acl_handle, ip, u64::try_from(ip_len).trapping_unwrap()) { + Ok((Some(body_handle), acl_error)) => { + unsafe { + *body_handle_out = body_handle; + *acl_error_out = acl_error; + } + FastlyStatus::OK + } + Ok((None, acl_error)) => { + unsafe { + *acl_error_out = acl_error; + } + FastlyStatus::OK + } + Err(e) => e.into(), + } + } +} + pub mod fastly_abi { use super::*; diff --git a/lib/compute-at-edge-abi/compute-at-edge.witx b/lib/compute-at-edge-abi/compute-at-edge.witx index 76bcd1aa..bb3e04e2 100644 --- a/lib/compute-at-edge-abi/compute-at-edge.witx +++ b/lib/compute-at-edge-abi/compute-at-edge.witx @@ -1155,3 +1155,19 @@ (result $err (expected $vcpu_ms (error $fastly_status))) ) ) + +(module $fastly_acl + (@interface func (export "open") + (param $name string) + (result $err (expected $acl_handle (error $fastly_status))) + ) + + (@interface func (export "lookup") + (param $acl $acl_handle) + (param $ip_octets (@witx const_pointer (@witx char8))) + (param $ip_len (@witx usize)) + (param $body_handle_out (@witx pointer $body_handle)) + (param $acl_error_out (@witx pointer $acl_error)) + (result $err (expected (error $fastly_status))) + ) +) diff --git a/lib/compute-at-edge-abi/typenames.witx b/lib/compute-at-edge-abi/typenames.witx index ecd268c1..b364b044 100644 --- a/lib/compute-at-edge-abi/typenames.witx +++ b/lib/compute-at-edge-abi/typenames.witx @@ -115,6 +115,8 @@ (typename $secret_store_handle (handle)) ;;; A handle to an individual secret. (typename $secret_handle (handle)) +;;; A handle to an ACL. +(typename $acl_handle (handle)) ;;; A handle to an object supporting generic async operations. ;;; Can be either a `body_handle` or a `pending_request_handle`. ;;; @@ -494,3 +496,17 @@ ;;; This will map to the api's 429 codes $too_many_requests )) + +(typename $acl_error + (enum (@witx tag u32) + ;;; The $acl_error has not been initialized. + $uninitialized + ;;; There was no error. + $ok + ;;; This will map to the api's 204 code. + ;;; It indicates that the request succeeded, yet returned nothing. + $no_content + ;;; This will map to the api's 429 code. + ;;; Too many requests have been made. + $too_many_requests + )) diff --git a/lib/data/viceroy-component-adapter.wasm b/lib/data/viceroy-component-adapter.wasm index 5493b851..69946d18 100755 Binary files a/lib/data/viceroy-component-adapter.wasm and b/lib/data/viceroy-component-adapter.wasm differ diff --git a/lib/src/acl.rs b/lib/src/acl.rs new file mode 100644 index 00000000..5ad01882 --- /dev/null +++ b/lib/src/acl.rs @@ -0,0 +1,405 @@ +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; +use std::fmt::Display; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::sync::Arc; + +/// Acls is a mapping of names to acl. +#[derive(Clone, Debug, Default)] +pub struct Acls { + acls: HashMap>, +} + +impl Acls { + pub fn new() -> Self { + Self { + acls: HashMap::new(), + } + } + + pub fn get_acl(&self, name: &str) -> Option<&Arc> { + self.acls.get(name) + } + + pub fn insert(&mut self, name: String, acl: Acl) { + self.acls.insert(name, Arc::new(acl)); + } +} + +/// An acl is a collection of acl entries. +/// +/// The JSON representation of this struct intentionally matches the JSON +/// format used to create/update ACLs via api.fastly.com. The goal being +/// to allow users to use the same JSON in Viceroy as in production. +/// +/// Example: +/// +/// ```json +/// { "entries": [ +/// { "op": "create", "prefix": "1.2.3.0/24", "action": "BLOCK" }, +/// { "op": "create", "prefix": "23.23.23.23/32", "action": "ALLOW" }, +/// { "op": "update", "prefix": "FACE::/32", "action": "ALLOW" } +/// ]} +/// ``` +/// +/// Note that, in Viceroy, the `op` field is ignored. +#[derive(Debug, Default, Deserialize)] +pub struct Acl { + pub(crate) entries: Vec, +} + +impl Acl { + /// Lookup performs a naive lookup of the given IP address + /// over the acls entries. + /// + /// If the IP matches multiple ACL entries, then: + /// - The most specific match is returned (longest mask), + /// - and in case of a tie, the last entry wins. + pub fn lookup(&self, ip: IpAddr) -> Option<&Entry> { + self.entries.iter().fold(None, |acc, entry| { + if let Some(mask) = entry.prefix.is_match(ip) { + if acc.is_none_or(|prev_match: &Entry| mask >= prev_match.prefix.mask) { + return Some(entry); + } + } + acc + }) + } +} + +/// An entry is an IP prefix and its associated action. +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub struct Entry { + prefix: Prefix, + action: Action, +} + +/// A prefix is an IP and network mask. +#[derive(Debug, PartialEq)] +pub struct Prefix { + ip: IpAddr, + mask: u8, +} + +impl Prefix { + pub(crate) fn new(ip: IpAddr, mask: u8) -> Self { + // Normalize IP based on mask. + let (ip, mask) = match ip { + IpAddr::V4(v4) => { + let mask = mask.clamp(1, 32); + let bit_mask = u32::MAX << (32 - mask); + ( + IpAddr::V4(Ipv4Addr::from_bits(v4.to_bits() & bit_mask)), + mask, + ) + } + IpAddr::V6(v6) => { + let mask = mask.clamp(1, 128); + let bit_mask = u128::MAX << (128 - mask); + ( + IpAddr::V6(Ipv6Addr::from_bits(v6.to_bits() & bit_mask)), + mask, + ) + } + }; + + Self { ip, mask } + } + + /// If the given IP matches the prefix, then the prefix's + /// mask is returned. + pub(crate) fn is_match(&self, ip: IpAddr) -> Option { + let masked = Self::new(ip, self.mask); + if masked.ip == self.ip { + Some(self.mask) + } else { + None + } + } +} + +impl Display for Prefix { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}/{}", self.ip, self.mask)) + } +} + +impl<'de> Deserialize<'de> for Prefix { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + let v = String::deserialize(de)?; + let (ip, mask) = v.split_once('/').ok_or(D::Error::custom(format!( + "invalid format '{}': want IP/MASK", + v + )))?; + + let mask = mask + .parse::() + .map_err(|err| D::Error::custom(format!("invalid prefix {}: {}", mask, err)))?; + + // Detect whether the IP is v4 or v6. + let ip = match ip.contains(':') { + false => { + if !(1..=32).contains(&mask) { + return Err(D::Error::custom(format!( + "mask outside allowed range [1, 32]: {}", + mask + ))); + } + ip.parse::().map(IpAddr::V4) + } + true => { + if !(1..=128).contains(&mask) { + return Err(D::Error::custom(format!( + "mask outside allowed range [1, 128]: {}", + mask + ))); + } + ip.parse::().map(IpAddr::V6) + } + } + .map_err(|err| D::Error::custom(format!("invalid ip address {}: {}", ip, err)))?; + + Ok(Self::new(ip, mask)) + } +} + +impl Serialize for Prefix { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(format!("{}", self).as_str()) + } +} + +const ACTION_ALLOW: &str = "ALLOW"; +const ACTION_BLOCK: &str = "BLOCK"; + +/// An action for a prefix. +#[derive(Clone, Debug, PartialEq)] +pub enum Action { + Allow, + Block, + Other(String), +} + +impl<'de> Deserialize<'de> for Action { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + let action = String::deserialize(de)?; + Ok(match action.to_uppercase().as_str() { + ACTION_ALLOW => Self::Allow, + ACTION_BLOCK => Self::Block, + _ => Self::Other(action), + }) + } +} + +impl Serialize for Action { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Allow => serializer.serialize_str(ACTION_ALLOW), + Self::Block => serializer.serialize_str(ACTION_BLOCK), + Self::Other(other) => serializer.serialize_str(format!("Other({})", other).as_str()), + } + } +} + +#[test] +fn prefix_is_match() { + let prefix = Prefix::new(Ipv4Addr::new(192, 168, 100, 0).into(), 16); + + assert_eq!( + prefix.is_match(Ipv4Addr::new(192, 168, 100, 0).into()), + Some(16) + ); + assert_eq!( + prefix.is_match(Ipv4Addr::new(192, 168, 200, 200).into()), + Some(16) + ); + + assert_eq!(prefix.is_match(Ipv4Addr::new(192, 167, 0, 0).into()), None); + assert_eq!(prefix.is_match(Ipv4Addr::new(192, 169, 0, 0).into()), None); + + let prefix = Prefix::new(Ipv6Addr::new(0xFACE, 0, 0, 0, 0, 0, 0, 0).into(), 16); + assert_eq!( + prefix.is_match(Ipv6Addr::new(0xFACE, 1, 2, 3, 4, 5, 6, 7).into()), + Some(16) + ); + + let v4 = Ipv4Addr::new(192, 168, 200, 200); + let v4_as_v6 = v4.to_ipv6_mapped(); + + assert_eq!(Prefix::new(v4.into(), 8).is_match(v4_as_v6.into()), None); + assert_eq!(Prefix::new(v4_as_v6.into(), 8).is_match(v4.into()), None); +} + +#[test] +fn acl_lookup() { + let acl = Acl { + entries: vec![ + Entry { + prefix: Prefix::new(Ipv4Addr::new(192, 168, 100, 0).into(), 16), + action: Action::Block, + }, + Entry { + prefix: Prefix::new(Ipv4Addr::new(192, 168, 100, 0).into(), 24), + action: Action::Block, + }, + Entry { + prefix: Prefix::new(Ipv4Addr::new(192, 168, 100, 0).into(), 8), + action: Action::Block, + }, + ], + }; + + match acl.lookup(Ipv4Addr::new(192, 168, 100, 1).into()) { + Some(lookup_match) => { + assert_eq!(acl.entries[1], *lookup_match); + } + None => panic!("expected lookup match"), + }; + + match acl.lookup(Ipv4Addr::new(192, 168, 200, 1).into()) { + Some(lookup_match) => { + assert_eq!(acl.entries[0], *lookup_match); + } + None => panic!("expected lookup match"), + }; + + match acl.lookup(Ipv4Addr::new(192, 1, 1, 1).into()) { + Some(lookup_match) => { + assert_eq!(acl.entries[2], *lookup_match); + } + None => panic!("expected lookup match"), + }; + + if let Some(lookup_match) = acl.lookup(Ipv4Addr::new(1, 1, 1, 1).into()) { + panic!("expected no lookup match, got {:?}", lookup_match) + }; +} + +#[test] +fn acl_json_parse() { + // In the following JSON, the `op` field should be ignored. It's included + // to assert that the JSON format used with api.fastly.com to create/modify + // ACLs can be used in Viceroy as well. + let input = r#" + { "entries": [ + { "op": "create", "prefix": "1.2.3.0/24", "action": "BLOCK" }, + { "op": "update", "prefix": "192.168.0.0/16", "action": "BLOCK" }, + { "op": "create", "prefix": "23.23.23.23/32", "action": "ALLOW" }, + { "op": "update", "prefix": "1.2.3.4/32", "action": "ALLOW" }, + { "op": "update", "prefix": "1.2.3.4/8", "action": "ALLOW" } + ]} + "#; + let acl: Acl = serde_json::from_str(input).expect("can decode"); + + let want = vec![ + Entry { + prefix: Prefix { + ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 0)), + mask: 24, + }, + action: Action::Block, + }, + Entry { + prefix: Prefix { + ip: IpAddr::V4(Ipv4Addr::new(192, 168, 0, 0)), + mask: 16, + }, + action: Action::Block, + }, + Entry { + prefix: Prefix { + ip: IpAddr::V4(Ipv4Addr::new(23, 23, 23, 23)), + mask: 32, + }, + action: Action::Allow, + }, + Entry { + prefix: Prefix { + ip: IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), + mask: 32, + }, + action: Action::Allow, + }, + Entry { + prefix: Prefix { + ip: IpAddr::V4(Ipv4Addr::new(1, 0, 0, 0)), + mask: 8, + }, + action: Action::Allow, + }, + ]; + + assert_eq!(acl.entries, want); +} + +#[test] +fn prefix_json_roundtrip() { + let assert_roundtrips = |input: &str, want: &str| { + let prefix: Prefix = + serde_json::from_str(format!("\"{}\"", input).as_str()).expect("can decode"); + let got = serde_json::to_string(&prefix).expect("can encode"); + assert_eq!( + got, + format!("\"{}\"", want), + "'{}' roundtrip: got {}, want {}", + input, + got, + want + ); + }; + + assert_roundtrips("255.255.255.255/32", "255.255.255.255/32"); + assert_roundtrips("255.255.255.255/8", "255.0.0.0/8"); + + assert_roundtrips("2002::1234:abcd:ffff:c0a8:101/64", "2002:0:0:1234::/64"); + assert_roundtrips("2000::AB/32", "2000::/32"); + + // Invalid prefix. + assert!(serde_json::from_str::("\"1.2.3.4/33\"").is_err()); + assert!(serde_json::from_str::("\"200::/129\"").is_err()); + assert!(serde_json::from_str::("\"200::/none\"").is_err()); + + // Invalid IP. + assert!(serde_json::from_str::("\"1.2.3.four/16\"").is_err()); + assert!(serde_json::from_str::("\"200::end/32\"").is_err()); + + // Invalid format. + assert!(serde_json::from_str::("\"1.2.3.4\"").is_err()); + assert!(serde_json::from_str::("\"200::\"").is_err()); +} + +#[test] +fn action_json_roundtrip() { + let assert_roundtrips = |input: &str, want: &str| { + let action: Action = + serde_json::from_str(format!("\"{}\"", input).as_str()).expect("can decode"); + let got = serde_json::to_string(&action).expect("can encode"); + assert_eq!( + got, + format!("\"{}\"", want), + "'{}' roundtrip: got {}, want {}", + input, + got, + want + ); + }; + + assert_roundtrips("ALLOW", "ALLOW"); + assert_roundtrips("allow", "ALLOW"); + assert_roundtrips("BLOCK", "BLOCK"); + assert_roundtrips("block", "BLOCK"); + assert_roundtrips("POTATO", "Other(POTATO)"); + assert_roundtrips("potato", "Other(potato)"); +} diff --git a/lib/src/component/acl.rs b/lib/src/component/acl.rs new file mode 100644 index 00000000..ff070392 --- /dev/null +++ b/lib/src/component/acl.rs @@ -0,0 +1,47 @@ +use super::fastly::api::{acl, http_body, types}; +use crate::linking::ComponentCtx; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[async_trait::async_trait] +impl acl::Host for ComponentCtx { + async fn open(&mut self, acl_name: Vec) -> Result { + let acl_name = String::from_utf8(acl_name)?; + let handle = self + .session + .acl_handle_by_name(&acl_name) + .ok_or(types::Error::OptionalNone)?; + Ok(handle.into()) + } + + async fn lookup( + &mut self, + acl_handle: acl::AclHandle, + ip_octets: Vec, + ip_len: u64, + ) -> Result<(Option, acl::AclError), types::Error> { + let acl = self + .session + .acl_by_handle(acl_handle.into()) + .ok_or(types::Error::BadHandle)?; + + let ip: IpAddr = match ip_len { + 4 => IpAddr::V4(Ipv4Addr::from( + TryInto::<[u8; 4]>::try_into(ip_octets).unwrap(), + )), + 16 => IpAddr::V6(Ipv6Addr::from( + TryInto::<[u8; 16]>::try_into(ip_octets).unwrap(), + )), + _ => return Err(types::Error::InvalidArgument), + }; + + match acl.lookup(ip) { + Some(entry) => { + let body = + serde_json::to_vec_pretty(&entry).map_err(|_| types::Error::GenericError)?; + let body_handle = self.session.insert_body(body.into()); + Ok((Some(body_handle.into()), acl::AclError::Ok)) + } + None => Ok((None, acl::AclError::NoContent)), + } + } +} diff --git a/lib/src/component/mod.rs b/lib/src/component/mod.rs index f137840a..ca8b0462 100644 --- a/lib/src/component/mod.rs +++ b/lib/src/component/mod.rs @@ -45,9 +45,12 @@ pub fn link_host_functions(linker: &mut component::Linker) -> anyh wasmtime_wasi::bindings::cli::stdout::add_to_linker_get_host(linker, wrap)?; wasmtime_wasi::bindings::cli::stderr::add_to_linker_get_host(linker, wrap)?; + fastly::api::acl::add_to_linker(linker, |x| x)?; fastly::api::async_io::add_to_linker(linker, |x| x)?; fastly::api::backend::add_to_linker(linker, |x| x)?; fastly::api::cache::add_to_linker(linker, |x| x)?; + fastly::api::compute_runtime::add_to_linker(linker, |x| x)?; + fastly::api::config_store::add_to_linker(linker, |x| x)?; fastly::api::device_detection::add_to_linker(linker, |x| x)?; fastly::api::dictionary::add_to_linker(linker, |x| x)?; fastly::api::erl::add_to_linker(linker, |x| x)?; @@ -56,19 +59,18 @@ pub fn link_host_functions(linker: &mut component::Linker) -> anyh fastly::api::http_req::add_to_linker(linker, |x| x)?; fastly::api::http_resp::add_to_linker(linker, |x| x)?; fastly::api::http_types::add_to_linker(linker, |x| x)?; + fastly::api::kv_store::add_to_linker(linker, |x| x)?; fastly::api::log::add_to_linker(linker, |x| x)?; fastly::api::object_store::add_to_linker(linker, |x| x)?; - fastly::api::kv_store::add_to_linker(linker, |x| x)?; fastly::api::purge::add_to_linker(linker, |x| x)?; fastly::api::secret_store::add_to_linker(linker, |x| x)?; fastly::api::types::add_to_linker(linker, |x| x)?; fastly::api::uap::add_to_linker(linker, |x| x)?; - fastly::api::config_store::add_to_linker(linker, |x| x)?; - fastly::api::compute_runtime::add_to_linker(linker, |x| x)?; Ok(()) } +pub mod acl; pub mod async_io; pub mod backend; pub mod cache; diff --git a/lib/src/config.rs b/lib/src/config.rs index 605187f3..85143477 100644 --- a/lib/src/config.rs +++ b/lib/src/config.rs @@ -2,7 +2,7 @@ use { self::{ - backends::BackendsConfig, dictionaries::DictionariesConfig, + acl::AclConfig, backends::BackendsConfig, dictionaries::DictionariesConfig, object_store::ObjectStoreConfig, secret_store::SecretStoreConfig, }, crate::error::FastlyConfigError, @@ -25,6 +25,10 @@ pub use self::dictionaries::{Dictionary, LoadedDictionary}; pub type Dictionaries = HashMap; +/// Types and deserializers for acl configuration settings. +mod acl; +pub use crate::acl::Acls; + /// Types and deserializers for backend configuration settings. mod backends; @@ -84,6 +88,11 @@ impl FastlyConfig { self.language.as_str() } + /// Get the acl configuration. + pub fn acls(&self) -> &Acls { + &self.local_server.acls.0 + } + /// Get the backend configuration. pub fn backends(&self) -> &Backends { &self.local_server.backends.0 @@ -191,6 +200,7 @@ impl TryInto for TomlFastlyConfig { /// may be added in the future. #[derive(Clone, Debug, Default)] pub struct LocalServerConfig { + acls: AclConfig, backends: BackendsConfig, device_detection: DeviceDetection, geolocation: Geolocation, @@ -211,6 +221,7 @@ pub enum ExperimentalModule { /// a [`LocalServerConfig`] with [`TryInto::try_into`]. #[derive(Deserialize)] struct RawLocalServerConfig { + acls: Option, backends: Option
, device_detection: Option
, geolocation: Option
, @@ -225,6 +236,7 @@ impl TryInto for RawLocalServerConfig { type Error = FastlyConfigError; fn try_into(self) -> Result { let Self { + acls, backends, device_detection, geolocation, @@ -232,6 +244,11 @@ impl TryInto for RawLocalServerConfig { object_stores, secret_stores, } = self; + let acls = if let Some(acls) = acls { + acls.try_into()? + } else { + AclConfig::default() + }; let backends = if let Some(backends) = backends { backends.try_into()? } else { @@ -264,6 +281,7 @@ impl TryInto for RawLocalServerConfig { }; Ok(LocalServerConfig { + acls, backends, device_detection, geolocation, diff --git a/lib/src/config/acl.rs b/lib/src/config/acl.rs new file mode 100644 index 00000000..559ffc94 --- /dev/null +++ b/lib/src/config/acl.rs @@ -0,0 +1,67 @@ +use crate::acl; + +#[derive(Clone, Debug, Default)] +pub struct AclConfig(pub(crate) acl::Acls); + +mod deserialization { + use { + super::AclConfig, + crate::acl, + crate::error::{AclConfigError, FastlyConfigError}, + std::path::PathBuf, + std::{convert::TryFrom, fs}, + toml::value::Table, + }; + + impl TryFrom
for AclConfig { + type Error = FastlyConfigError; + fn try_from(toml: Table) -> Result { + let mut acls = acl::Acls::new(); + + for (name, value) in toml.iter() { + // Here we allow each table entry to be either a: + // - string: path to JSON file + // - table: must have a 'file' entry, which is the path to JSON file + let path = if let Some(path) = value.as_str() { + path + } else if let Some(tbl) = value.as_table() { + tbl.get("file") + .ok_or(FastlyConfigError::InvalidAclDefinition { + name: name.to_string(), + err: AclConfigError::MissingFile, + })? + .as_str() + .ok_or(FastlyConfigError::InvalidAclDefinition { + name: name.to_string(), + err: AclConfigError::MissingFile, + })? + } else { + return Err(FastlyConfigError::InvalidAclDefinition { + name: name.to_string(), + err: AclConfigError::InvalidType, + }); + }; + + let acl: acl::Acl = { + let path = PathBuf::from(path); + let fd = fs::File::open(path).map_err(|err| { + FastlyConfigError::InvalidAclDefinition { + name: name.to_string(), + err: AclConfigError::IoError(err), + } + })?; + serde_json::from_reader(fd).map_err(|err| { + FastlyConfigError::InvalidAclDefinition { + name: name.to_string(), + err: AclConfigError::JsonError(err), + } + })? + }; + + acls.insert(name.to_string(), acl); + } + + Ok(Self(acls)) + } + } +} diff --git a/lib/src/error.rs b/lib/src/error.rs index a7cd2fc0..4eea2cb9 100644 --- a/lib/src/error.rs +++ b/lib/src/error.rs @@ -299,6 +299,10 @@ pub enum HandleError { /// An async item handle was not valid. #[error("Invalid async item handle: {0}")] InvalidAsyncItemHandle(crate::wiggle_abi::types::AsyncItemHandle), + + /// An acl handle was not valid. + #[error("Invalid acl handle: {0}")] + InvalidAclHandle(crate::wiggle_abi::types::AclHandle), } /// Errors that can occur in a worker thread running a guest module. @@ -356,6 +360,13 @@ pub enum FastlyConfigError { err: GeolocationConfigError, }, + #[error("invalid configuration for '{name}': {err}")] + InvalidAclDefinition { + name: String, + #[source] + err: AclConfigError, + }, + #[error("invalid configuration for '{name}': {err}")] InvalidBackendDefinition { name: String, @@ -400,6 +411,24 @@ pub enum FastlyConfigError { InvalidManifestVersion(#[from] semver::SemVerError), } +/// Errors that may occur while validating acl configurations. +#[derive(Debug, thiserror::Error)] +pub enum AclConfigError { + /// An I/O error that occurred while processing a file. + #[error(transparent)] + IoError(std::io::Error), + + /// An error occurred parsing JSON. + #[error(transparent)] + JsonError(serde_json::error::Error), + + #[error("acl must be a TOML table or string")] + InvalidType, + + #[error("missing 'file' field")] + MissingFile, +} + /// Errors that may occur while validating backend configurations. #[derive(Debug, thiserror::Error)] pub enum BackendConfigError { diff --git a/lib/src/execute.rs b/lib/src/execute.rs index a8aaee0c..c66dd5bc 100644 --- a/lib/src/execute.rs +++ b/lib/src/execute.rs @@ -2,6 +2,7 @@ use { crate::{ + acl::Acls, adapt, body::Body, component as compute, @@ -72,6 +73,8 @@ pub struct ExecuteCtx { engine: Engine, /// An almost-linked Instance: each import function is linked, just needs a Store instance_pre: Arc, + /// The acls for this execution. + acls: Arc, /// The backends for this execution. backends: Arc, /// The device detection mappings for this execution. @@ -208,6 +211,7 @@ impl ExecuteCtx { Ok(Self { engine, instance_pre: Arc::new(instance_pre), + acls: Arc::new(Acls::new()), backends: Arc::new(Backends::default()), device_detection: Arc::new(DeviceDetection::default()), geolocation: Arc::new(Geolocation::default()), @@ -231,6 +235,17 @@ impl ExecuteCtx { &self.engine } + /// Get the acls for this execution context. + pub fn acls(&self) -> &Acls { + &self.acls + } + + /// Set the acls for this execution context. + pub fn with_acls(mut self, acls: Acls) -> Self { + self.acls = Arc::new(acls); + self + } + /// Get the backends for this execution context. pub fn backends(&self) -> &Backends { &self.backends @@ -461,6 +476,7 @@ impl ExecuteCtx { remote, active_cpu_time_us, &self, + self.acls.clone(), self.backends.clone(), self.device_detection.clone(), self.geolocation.clone(), @@ -619,6 +635,7 @@ impl ExecuteCtx { remote, active_cpu_time_us.clone(), &self, + self.acls.clone(), self.backends.clone(), self.device_detection.clone(), self.geolocation.clone(), diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 6bf2811c..d35b11a7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -21,6 +21,7 @@ pub mod error; pub mod logging; pub mod session; +mod acl; mod async_io; pub mod component; mod downstream; diff --git a/lib/src/linking.rs b/lib/src/linking.rs index 29c058e0..2033fa71 100644 --- a/lib/src/linking.rs +++ b/lib/src/linking.rs @@ -287,25 +287,26 @@ pub fn link_host_functions( wasmtime_wasi::preview1::add_to_linker_async(linker, WasmCtx::wasi)?; wiggle_abi::fastly_abi::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_acl::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_async_io::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_backend::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_cache::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_compute_runtime::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_config_store::add_to_linker(linker, WasmCtx::session)?; - wiggle_abi::fastly_dictionary::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_device_detection::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_dictionary::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_erl::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_geo::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_http_body::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_http_cache::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_http_req::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_http_resp::add_to_linker(linker, WasmCtx::session)?; + wiggle_abi::fastly_kv_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_log::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_object_store::add_to_linker(linker, WasmCtx::session)?; - wiggle_abi::fastly_kv_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_purge::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_secret_store::add_to_linker(linker, WasmCtx::session)?; wiggle_abi::fastly_uap::add_to_linker(linker, WasmCtx::session)?; - wiggle_abi::fastly_async_io::add_to_linker(linker, WasmCtx::session)?; - wiggle_abi::fastly_backend::add_to_linker(linker, WasmCtx::session)?; - wiggle_abi::fastly_compute_runtime::add_to_linker(linker, WasmCtx::session)?; link_legacy_aliases(linker)?; Ok(()) } diff --git a/lib/src/session.rs b/lib/src/session.rs index 601c1033..d8ddc8f9 100644 --- a/lib/src/session.rs +++ b/lib/src/session.rs @@ -22,6 +22,7 @@ use crate::object_store::KvStoreError; use { self::downstream::DownstreamResponse, crate::{ + acl::{Acl, Acls}, body::Body, config::{Backend, Backends, DeviceDetection, Dictionaries, Geolocation, LoadedDictionary}, error::{Error, HandleError}, @@ -31,11 +32,11 @@ use { streaming_body::StreamingBody, upstream::{SelectTarget, TlsConfig}, wiggle_abi::types::{ - self, BodyHandle, ContentEncodings, DictionaryHandle, EndpointHandle, KvInsertMode, - KvStoreDeleteHandle, KvStoreHandle, KvStoreInsertHandle, KvStoreListHandle, - KvStoreLookupHandle, PendingKvDeleteHandle, PendingKvInsertHandle, PendingKvListHandle, - PendingKvLookupHandle, PendingRequestHandle, RequestHandle, ResponseHandle, - SecretHandle, SecretStoreHandle, + self, AclHandle, BodyHandle, ContentEncodings, DictionaryHandle, EndpointHandle, + KvInsertMode, KvStoreDeleteHandle, KvStoreHandle, KvStoreInsertHandle, + KvStoreListHandle, KvStoreLookupHandle, PendingKvDeleteHandle, PendingKvInsertHandle, + PendingKvListHandle, PendingKvLookupHandle, PendingRequestHandle, RequestHandle, + ResponseHandle, SecretHandle, SecretStoreHandle, }, ExecuteCtx, }, @@ -97,6 +98,12 @@ pub struct Session { log_endpoints: PrimaryMap, /// A by-name map for logging endpoints. log_endpoints_by_name: HashMap, EndpointHandle>, + /// The ACLs configured for this execution. + /// + /// Populated prior to guest execution, and never modified. + acls: Arc, + /// Active ACL handles. + acl_handles: PrimaryMap>, /// The backends configured for this execution. /// /// Populated prior to guest execution, and never modified. @@ -164,6 +171,7 @@ impl Session { client_addr: SocketAddr, active_cpu_time_us: Arc, ctx: &ExecuteCtx, + acls: Arc, backends: Arc, device_detection: Arc, geolocation: Arc, @@ -197,6 +205,8 @@ impl Session { capture_logs: ctx.capture_logs(), log_endpoints: PrimaryMap::new(), log_endpoints_by_name: HashMap::new(), + acls, + acl_handles: PrimaryMap::new(), backends, device_detection, geolocation, @@ -591,6 +601,17 @@ impl Session { .ok_or(HandleError::InvalidEndpointHandle(handle)) } + // ----- ACLs API ----- + + pub fn acl_handle_by_name(&mut self, name: &str) -> Option { + let acl = self.acls.get_acl(name)?; + Some(self.acl_handles.push(acl.clone())) + } + + pub fn acl_by_handle(&self, handle: AclHandle) -> Option> { + self.acl_handles.get(handle).map(Arc::clone) + } + // ----- Backends API ----- /// Look up a backend by name. diff --git a/lib/src/wiggle_abi.rs b/lib/src/wiggle_abi.rs index c7f2c069..46c6bb88 100644 --- a/lib/src/wiggle_abi.rs +++ b/lib/src/wiggle_abi.rs @@ -48,6 +48,7 @@ macro_rules! multi_value_result { }}; } +mod acl; mod backend_impl; mod body_impl; mod cache; @@ -76,6 +77,7 @@ wiggle::from_witx!({ witx: ["$CARGO_MANIFEST_DIR/compute-at-edge-abi/compute-at-edge.witx"], errors: { fastly_status => Error }, async: { + fastly_acl::lookup, fastly_async_io::{select}, fastly_object_store::{delete_async, pending_delete_wait, insert, insert_async, pending_insert_wait, lookup_async, pending_lookup_wait, list}, fastly_kv_store::{lookup, lookup_wait, lookup_wait_v2, insert, insert_wait, delete, delete_wait, list, list_wait}, diff --git a/lib/src/wiggle_abi/acl.rs b/lib/src/wiggle_abi/acl.rs new file mode 100644 index 00000000..d12cd83e --- /dev/null +++ b/lib/src/wiggle_abi/acl.rs @@ -0,0 +1,65 @@ +use crate::error::{Error, HandleError}; +use crate::session::Session; +use crate::wiggle_abi::{fastly_acl, types}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[wiggle::async_trait] +impl fastly_acl::FastlyAcl for Session { + /// Open a handle to an ACL by its linked name. + fn open( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + acl_name: wiggle::GuestPtr, + ) -> Result { + let acl_name = memory.as_str(acl_name)?.ok_or(Error::SharedMemory)?; + self.acl_handle_by_name(acl_name).ok_or(Error::ValueAbsent) + } + + /// Perform an ACL lookup operation using the given ACL handle. + /// + /// There are two levels of errors returned by this function: + /// - Error: These are general hostcall errors, e.g. handle not found. + /// - AclError: There are ACL-specific errors, e.g. 'no content'. + /// It's the callers responsibility to check both errors. + async fn lookup( + &mut self, + memory: &mut wiggle::GuestMemory<'_>, + acl_handle: types::AclHandle, + ip_octets: wiggle::GuestPtr, // This should be either a 4 or 16-byte array. + ip_len: u32, // Either 4 or 16. + body_handle_out: wiggle::GuestPtr, + acl_error_out: wiggle::GuestPtr, + ) -> Result<(), Error> { + let acl = self.acl_by_handle(acl_handle).ok_or(Error::HandleError( + HandleError::InvalidAclHandle(acl_handle), + ))?; + + let ip: IpAddr = { + let ip_octets = memory.to_vec(ip_octets.as_array(ip_len))?; + match ip_len { + 4 => IpAddr::V4(Ipv4Addr::from( + TryInto::<[u8; 4]>::try_into(ip_octets).unwrap(), + )), + 16 => IpAddr::V6(Ipv6Addr::from( + TryInto::<[u8; 16]>::try_into(ip_octets).unwrap(), + )), + _ => return Err(Error::InvalidArgument), + } + }; + + match acl.lookup(ip) { + Some(entry) => { + let body = + serde_json::to_vec_pretty(&entry).map_err(|err| Error::Other(err.into()))?; + let body_handle = self.insert_body(body.into()); + memory.write(body_handle_out, body_handle)?; + memory.write(acl_error_out, types::AclError::Ok)?; + Ok(()) + } + None => { + memory.write(acl_error_out, types::AclError::NoContent)?; + Ok(()) + } + } + } +} diff --git a/lib/src/wiggle_abi/entity.rs b/lib/src/wiggle_abi/entity.rs index 5b11940d..00547165 100644 --- a/lib/src/wiggle_abi/entity.rs +++ b/lib/src/wiggle_abi/entity.rs @@ -3,7 +3,7 @@ //! [ref]: https://docs.rs/cranelift-entity/latest/cranelift_entity/trait.EntityRef.html use super::types::{ - AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, KvStoreHandle, + AclHandle, AsyncItemHandle, BodyHandle, DictionaryHandle, EndpointHandle, KvStoreHandle, ObjectStoreHandle, PendingRequestHandle, RequestHandle, ResponseHandle, SecretHandle, SecretStoreHandle, }; @@ -40,14 +40,15 @@ macro_rules! wiggle_entity { }; } +wiggle_entity!(AclHandle); +wiggle_entity!(AsyncItemHandle); wiggle_entity!(BodyHandle); -wiggle_entity!(RequestHandle); -wiggle_entity!(ResponseHandle); -wiggle_entity!(EndpointHandle); -wiggle_entity!(PendingRequestHandle); wiggle_entity!(DictionaryHandle); -wiggle_entity!(ObjectStoreHandle); +wiggle_entity!(EndpointHandle); wiggle_entity!(KvStoreHandle); -wiggle_entity!(SecretStoreHandle); +wiggle_entity!(ObjectStoreHandle); +wiggle_entity!(PendingRequestHandle); +wiggle_entity!(RequestHandle); +wiggle_entity!(ResponseHandle); wiggle_entity!(SecretHandle); -wiggle_entity!(AsyncItemHandle); +wiggle_entity!(SecretStoreHandle); diff --git a/lib/wit/deps/fastly/compute.wit b/lib/wit/deps/fastly/compute.wit index e5f2342f..a336c344 100644 --- a/lib/wit/deps/fastly/compute.wit +++ b/lib/wit/deps/fastly/compute.wit @@ -850,6 +850,32 @@ interface secret-store { from-bytes: func(bytes: list) -> result; } +/* + * Fastly ACL + */ +interface acl { + + use types.{error}; + use http-types.{body-handle}; + + type acl-handle = u32; + + enum acl-error { + uninitialized, + ok, + no-content, + too-many-requests, + } + + open: func(name: list) -> result; + + lookup: func( + acl: acl-handle, + ip-octets: list, + ip-len: u64, + ) -> result, acl-error>, error>; +} + /* * Fastly backend */ @@ -1338,6 +1364,7 @@ world compute { import wasi:cli/stderr@0.2.0; import wasi:cli/stdin@0.2.0; + import acl; import async-io; import backend; import cache; diff --git a/test-fixtures/Cargo.toml b/test-fixtures/Cargo.toml index 3ff10508..e9701691 100644 --- a/test-fixtures/Cargo.toml +++ b/test-fixtures/Cargo.toml @@ -7,6 +7,11 @@ edition = "2021" license = "Apache-2.0 WITH LLVM-exception" publish = false +[features] +# Temporary feature used until the fastly SDK is updated +# to a version which contains the fastly_acl hostcalls. +acl_hostcalls = [] + [dependencies] anyhow = "1.0.86" base64 = "0.21.2" diff --git a/test-fixtures/data/my-acl-1.json b/test-fixtures/data/my-acl-1.json new file mode 100644 index 00000000..ee44a2e9 --- /dev/null +++ b/test-fixtures/data/my-acl-1.json @@ -0,0 +1,8 @@ +{ + "entries": [ + { "op": "update", "prefix": "1.2.3.0/24", "action": "BLOCK" }, + { "op": "create", "prefix": "192.168.0.0/16", "action": "BLOCK" }, + { "op": "update", "prefix": "23.23.23.23/32", "action": "ALLOW" }, + { "op": "create", "prefix": "1.2.3.4/32", "action": "ALLOW" } + ] +} diff --git a/test-fixtures/data/my-acl-2.json b/test-fixtures/data/my-acl-2.json new file mode 100644 index 00000000..113a33e6 --- /dev/null +++ b/test-fixtures/data/my-acl-2.json @@ -0,0 +1,6 @@ +{ + "entries": [ + { "op": "update", "prefix": "2000::/24", "action": "BLOCK" }, + { "op": "create", "prefix": "FACE::/16", "action": "ALLOW" } + ] +} diff --git a/test-fixtures/src/bin/acl.rs b/test-fixtures/src/bin/acl.rs new file mode 100644 index 00000000..a21b74ef --- /dev/null +++ b/test-fixtures/src/bin/acl.rs @@ -0,0 +1,62 @@ +//! A guest program to test that acls works properly. +use fastly::Error; + +fn main() -> Result<(), Error> { + // Temporary until fastly SDK is released which + // includes the fastly::acl module. + #[cfg(feature = "acl_hostcalls")] + { + use fastly::acl::Acl; + use std::net::{Ipv4Addr, Ipv6Addr}; + + match Acl::open("DOES-NOT-EXIST") { + Err(fastly::acl::OpenError::AclNotFound) => { /* OK */ } + Err(other) => panic!("expected error opening non-existant acl, got: {:?}", other), + _ => panic!("expected error opening non-existant acl, got Ok"), + } + + let acl1 = Acl::open("my-acl-1")?; + + match acl1.try_lookup(Ipv4Addr::new(192, 168, 1, 1).into())? { + Some(lookup_match) => { + assert_eq!(lookup_match.prefix(), "192.168.0.0/16"); + assert!(lookup_match.is_block()); + } + None => panic!("expected match"), + }; + match acl1.try_lookup(Ipv4Addr::new(23, 23, 23, 23).into())? { + Some(lookup_match) => { + assert_eq!(lookup_match.prefix(), "23.23.23.23/32"); + assert!(lookup_match.is_allow()); + } + None => panic!("expected match"), + }; + if let Some(lookup_match) = acl1.try_lookup(Ipv4Addr::new(100, 100, 100, 100).into())? { + panic!("expected no match, got: {:?}", lookup_match); + } + + let acl2 = Acl::open("my-acl-2")?; + + match acl2.try_lookup(Ipv6Addr::new(0x2000, 0, 0, 0, 0, 1, 2, 3).into())? { + Some(lookup_match) => { + assert_eq!(lookup_match.prefix(), "2000::/24"); + assert!(lookup_match.is_block()); + } + None => panic!("expected match"), + }; + match acl2.try_lookup(Ipv6Addr::new(0xFACE, 0, 2, 3, 4, 5, 6, 7).into())? { + Some(lookup_match) => { + assert_eq!(lookup_match.prefix(), "face::/16"); + assert!(lookup_match.is_allow()); + } + None => panic!("expected match"), + }; + if let Some(lookup_match) = + acl2.try_lookup(Ipv6Addr::new(0xFADE, 1, 2, 3, 4, 5, 6, 7).into())? + { + panic!("expected no match, got: {:?}", lookup_match); + }; + } + + Ok(()) +}