Add destination domain ACL rules
This commit is contained in:
+291
-13
@@ -59,11 +59,95 @@ impl SaltedHash {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub(crate) enum DestinationHost {
|
||||
Domain(String),
|
||||
Ip(IpAddr),
|
||||
}
|
||||
|
||||
impl DestinationHost {
|
||||
pub fn parse(host: &str) -> Option<Self> {
|
||||
if let Some(ipv6) = host
|
||||
.strip_prefix('[')
|
||||
.and_then(|host| host.strip_suffix(']'))
|
||||
{
|
||||
return ipv6.parse().ok().map(Self::Ip);
|
||||
}
|
||||
|
||||
let ip_candidate = host.strip_suffix('.').unwrap_or(host);
|
||||
if let Ok(ip) = ip_candidate.parse() {
|
||||
return Some(Self::Ip(ip));
|
||||
}
|
||||
if host.contains(':') {
|
||||
return None;
|
||||
}
|
||||
|
||||
normalize_domain(host).map(Self::Domain)
|
||||
}
|
||||
}
|
||||
|
||||
enum DomainPattern {
|
||||
Exact(String),
|
||||
Subdomain(String),
|
||||
}
|
||||
|
||||
impl DomainPattern {
|
||||
fn parse(pattern: &str) -> Option<Self> {
|
||||
let (subdomain, domain) = pattern
|
||||
.strip_prefix("*.")
|
||||
.map_or((false, pattern), |domain| (true, domain));
|
||||
if matches!(DestinationHost::parse(domain), Some(DestinationHost::Ip(_))) {
|
||||
return None;
|
||||
}
|
||||
let domain = normalize_domain(domain)?;
|
||||
if subdomain {
|
||||
Some(Self::Subdomain(domain))
|
||||
} else {
|
||||
Some(Self::Exact(domain))
|
||||
}
|
||||
}
|
||||
|
||||
fn matches(&self, destination: &DestinationHost) -> bool {
|
||||
let DestinationHost::Domain(domain) = destination else {
|
||||
return false;
|
||||
};
|
||||
match self {
|
||||
Self::Exact(expected) => domain == expected,
|
||||
Self::Subdomain(suffix) => domain
|
||||
.strip_suffix(suffix)
|
||||
.is_some_and(|prefix| prefix.ends_with('.')),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_domain(domain: &str) -> Option<String> {
|
||||
let domain = domain.strip_suffix('.').unwrap_or(domain);
|
||||
if domain.is_empty() || domain.len() > 253 || !domain.is_ascii() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for label in domain.split('.') {
|
||||
if label.is_empty()
|
||||
|| label.len() > 63
|
||||
|| !label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||
|| !label.as_bytes()[0].is_ascii_alphanumeric()
|
||||
|| !label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(domain.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
struct Rule {
|
||||
name: String,
|
||||
action: Action,
|
||||
users: BTreeSet<String>,
|
||||
networks: Vec<IpNet>,
|
||||
domains: Vec<DomainPattern>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -92,11 +176,19 @@ pub(crate) struct AclOutcome {
|
||||
|
||||
impl AclOutcome {
|
||||
fn rule(rule: &Rule) -> Self {
|
||||
let reason = match (rule.users.is_empty(), rule.networks.is_empty()) {
|
||||
(false, false) => "user_and_network",
|
||||
(false, true) => "user",
|
||||
(true, false) => "network",
|
||||
(true, true) => unreachable!("empty ACL rules are rejected"),
|
||||
let reason = match (
|
||||
rule.users.is_empty(),
|
||||
rule.networks.is_empty(),
|
||||
rule.domains.is_empty(),
|
||||
) {
|
||||
(false, false, false) => "user_and_network_and_domain",
|
||||
(false, false, true) => "user_and_network",
|
||||
(false, true, false) => "user_and_domain",
|
||||
(false, true, true) => "user",
|
||||
(true, false, false) => "network_and_domain",
|
||||
(true, false, true) => "network",
|
||||
(true, true, false) => "domain",
|
||||
(true, true, true) => unreachable!("empty ACL rules are rejected"),
|
||||
};
|
||||
Self {
|
||||
decision: match rule.action {
|
||||
@@ -156,7 +248,12 @@ impl Acl {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn authorize(&self, peer: IpAddr, headers: &HeaderMap) -> AclOutcome {
|
||||
pub fn authorize(
|
||||
&self,
|
||||
peer: IpAddr,
|
||||
headers: &HeaderMap,
|
||||
destination: Option<&DestinationHost>,
|
||||
) -> AclOutcome {
|
||||
let credentials = match parse_basic_credentials(headers) {
|
||||
Credentials::Missing => None,
|
||||
Credentials::Invalid => return AclOutcome::challenge("invalid_credentials"),
|
||||
@@ -177,7 +274,7 @@ impl Acl {
|
||||
};
|
||||
|
||||
for rule in &self.rules {
|
||||
if rule_matches(rule, peer, authenticated_user.as_deref()) {
|
||||
if rule_matches(rule, peer, authenticated_user.as_deref(), destination) {
|
||||
return AclOutcome::rule(rule);
|
||||
}
|
||||
}
|
||||
@@ -197,7 +294,7 @@ fn compile_rule(rule: RuleConfig, users: &BTreeMap<String, User>) -> Result<Rule
|
||||
if rule.name.trim().is_empty() {
|
||||
return Err(AclError::EmptyRuleName);
|
||||
}
|
||||
if rule.users.is_empty() && rule.networks.is_empty() {
|
||||
if rule.users.is_empty() && rule.networks.is_empty() && rule.domains.is_empty() {
|
||||
return Err(AclError::EmptyRule(rule.name));
|
||||
}
|
||||
|
||||
@@ -219,20 +316,41 @@ fn compile_rule(rule: RuleConfig, users: &BTreeMap<String, User>) -> Result<Rule
|
||||
networks.push(parsed);
|
||||
}
|
||||
|
||||
let mut domains = Vec::with_capacity(rule.domains.len());
|
||||
for domain in rule.domains {
|
||||
let parsed = DomainPattern::parse(&domain).ok_or_else(|| AclError::InvalidDomain {
|
||||
rule: rule.name.clone(),
|
||||
domain,
|
||||
})?;
|
||||
domains.push(parsed);
|
||||
}
|
||||
|
||||
Ok(Rule {
|
||||
name: rule.name,
|
||||
action: rule.action,
|
||||
users: rule.users.into_iter().collect(),
|
||||
networks,
|
||||
domains,
|
||||
})
|
||||
}
|
||||
|
||||
fn rule_matches(rule: &Rule, peer: IpAddr, authenticated_user: Option<&str>) -> bool {
|
||||
fn rule_matches(
|
||||
rule: &Rule,
|
||||
peer: IpAddr,
|
||||
authenticated_user: Option<&str>,
|
||||
destination: Option<&DestinationHost>,
|
||||
) -> bool {
|
||||
let user_matches = rule.users.is_empty()
|
||||
|| authenticated_user.is_some_and(|username| rule.users.contains(username));
|
||||
let network_matches =
|
||||
rule.networks.is_empty() || rule.networks.iter().any(|network| network.contains(&peer));
|
||||
user_matches && network_matches
|
||||
let domain_matches = rule.domains.is_empty()
|
||||
|| destination.is_some_and(|destination| {
|
||||
rule.domains
|
||||
.iter()
|
||||
.any(|pattern| pattern.matches(destination))
|
||||
});
|
||||
user_matches && network_matches && domain_matches
|
||||
}
|
||||
|
||||
enum Credentials {
|
||||
@@ -328,6 +446,8 @@ pub enum AclError {
|
||||
UnknownUser { rule: String, username: String },
|
||||
#[error("ACL rule {rule:?} has invalid network {network:?}")]
|
||||
InvalidNetwork { rule: String, network: String },
|
||||
#[error("ACL rule {rule:?} has invalid domain pattern {domain:?}")]
|
||||
InvalidDomain { rule: String, domain: String },
|
||||
#[error("ACL user {0:?} has an invalid or unsupported salted SHA-256 password hash")]
|
||||
InvalidPasswordHash(String),
|
||||
}
|
||||
@@ -347,6 +467,7 @@ mod tests {
|
||||
action,
|
||||
users: vec![username.to_owned()],
|
||||
networks: vec![],
|
||||
domains: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +495,7 @@ mod tests {
|
||||
let outcome = acl.authorize(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
&headers("alice", "correct horse battery staple"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(outcome.decision, Decision::Allow);
|
||||
assert_eq!(outcome.reason, "user");
|
||||
@@ -392,7 +514,7 @@ mod tests {
|
||||
headers("alice", "wrong"),
|
||||
malformed,
|
||||
] {
|
||||
let outcome = acl.authorize("127.0.0.1".parse().unwrap(), &headers);
|
||||
let outcome = acl.authorize("127.0.0.1".parse().unwrap(), &headers, None);
|
||||
assert_eq!(outcome.decision, Decision::Challenge);
|
||||
}
|
||||
}
|
||||
@@ -407,14 +529,15 @@ mod tests {
|
||||
action: Action::Allow,
|
||||
users: vec![],
|
||||
networks: vec!["127.0.0.0/8".to_owned(), "::1/128".to_owned()],
|
||||
domains: vec![],
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let allowed = acl.authorize("127.0.0.2".parse().unwrap(), &HeaderMap::new());
|
||||
let allowed = acl.authorize("127.0.0.2".parse().unwrap(), &HeaderMap::new(), None);
|
||||
assert_eq!(allowed.decision, Decision::Allow);
|
||||
|
||||
let denied = acl.authorize("192.0.2.1".parse().unwrap(), &HeaderMap::new());
|
||||
let denied = acl.authorize("192.0.2.1".parse().unwrap(), &HeaderMap::new(), None);
|
||||
assert_eq!(denied.decision, Decision::Deny);
|
||||
}
|
||||
|
||||
@@ -430,6 +553,7 @@ mod tests {
|
||||
action: Action::Deny,
|
||||
users: vec!["alice".to_owned()],
|
||||
networks: vec!["192.0.2.0/24".to_owned()],
|
||||
domains: vec![],
|
||||
},
|
||||
user_rule("allow-alice", "alice", Action::Allow),
|
||||
],
|
||||
@@ -439,6 +563,7 @@ mod tests {
|
||||
let denied = acl.authorize(
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
&headers("alice", "correct horse battery staple"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(denied.decision, Decision::Deny);
|
||||
assert_eq!(denied.rule.as_deref(), Some("deny-alice-off-network"));
|
||||
@@ -446,10 +571,162 @@ mod tests {
|
||||
let allowed = acl.authorize(
|
||||
"198.51.100.10".parse().unwrap(),
|
||||
&headers("alice", "correct horse battery staple"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(allowed.decision, Decision::Allow);
|
||||
}
|
||||
|
||||
fn destination(host: &str) -> DestinationHost {
|
||||
DestinationHost::parse(host).unwrap()
|
||||
}
|
||||
|
||||
fn domain_acl(default_action: Action, action: Action, domains: &[&str]) -> Acl {
|
||||
Acl::from_config(AclConfig {
|
||||
default_action,
|
||||
users: BTreeMap::new(),
|
||||
rules: vec![RuleConfig {
|
||||
name: "domain-rule".to_owned(),
|
||||
action,
|
||||
users: vec![],
|
||||
networks: vec![],
|
||||
domains: domains.iter().map(|domain| (*domain).to_owned()).collect(),
|
||||
}],
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_domains_and_matches_wildcard_label_boundaries() {
|
||||
let exact = DomainPattern::parse("ExAmple.COM.").unwrap();
|
||||
assert!(exact.matches(&destination("example.com")));
|
||||
assert!(exact.matches(&destination("EXAMPLE.COM.")));
|
||||
assert!(!exact.matches(&destination("www.example.com")));
|
||||
|
||||
let wildcard = DomainPattern::parse("*.ExAmple.COM.").unwrap();
|
||||
assert!(wildcard.matches(&destination("www.example.com")));
|
||||
assert!(wildcard.matches(&destination("a.b.example.com")));
|
||||
assert!(!wildcard.matches(&destination("example.com")));
|
||||
assert!(!wildcard.matches(&destination("evil-example.com")));
|
||||
assert!(!wildcard.matches(&destination("127.0.0.1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforces_domain_whitelist_including_ip_literal_denial() {
|
||||
let acl = domain_acl(
|
||||
Action::Deny,
|
||||
Action::Allow,
|
||||
&["example.com", "*.rust-lang.org"],
|
||||
);
|
||||
|
||||
let allowed = acl.authorize(
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination("example.com")),
|
||||
);
|
||||
assert_eq!(allowed.decision, Decision::Allow);
|
||||
assert_eq!(allowed.reason, "domain");
|
||||
|
||||
let nested = acl.authorize(
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination("doc.rust-lang.org")),
|
||||
);
|
||||
assert_eq!(nested.decision, Decision::Allow);
|
||||
|
||||
for host in ["rust-lang.org", "unrelated.example", "127.0.0.1"] {
|
||||
let denied = acl.authorize(
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination(host)),
|
||||
);
|
||||
assert_eq!(denied.decision, Decision::Deny, "host: {host}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enforces_domain_blacklist_without_matching_ip_literals() {
|
||||
let acl = domain_acl(Action::Allow, Action::Deny, &["ads.example.net"]);
|
||||
|
||||
let denied = acl.authorize(
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination("ADS.EXAMPLE.NET.")),
|
||||
);
|
||||
assert_eq!(denied.decision, Decision::Deny);
|
||||
|
||||
for host in ["www.example.net", "192.0.2.10"] {
|
||||
let allowed = acl.authorize(
|
||||
"192.0.2.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination(host)),
|
||||
);
|
||||
assert_eq!(allowed.decision, Decision::Allow, "host: {host}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combines_user_and_domain_selectors() {
|
||||
let password_hash = hash_password(PASSWORD).unwrap();
|
||||
let acl = Acl::from_config(AclConfig {
|
||||
default_action: Action::Deny,
|
||||
users: BTreeMap::from([("alice".to_owned(), UserConfig { password_hash })]),
|
||||
rules: vec![RuleConfig {
|
||||
name: "alice-example".to_owned(),
|
||||
action: Action::Allow,
|
||||
users: vec!["alice".to_owned()],
|
||||
networks: vec![],
|
||||
domains: vec!["example.com".to_owned()],
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let allowed = acl.authorize(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
&headers("alice", "correct horse battery staple"),
|
||||
Some(&destination("example.com")),
|
||||
);
|
||||
assert_eq!(allowed.decision, Decision::Allow);
|
||||
assert_eq!(allowed.reason, "user_and_domain");
|
||||
|
||||
let denied = acl.authorize(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
&headers("alice", "correct horse battery staple"),
|
||||
Some(&destination("other.example")),
|
||||
);
|
||||
assert_eq!(denied.decision, Decision::Deny);
|
||||
|
||||
let challenged = acl.authorize(
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
&HeaderMap::new(),
|
||||
Some(&destination("example.com")),
|
||||
);
|
||||
assert_eq!(challenged.decision, Decision::Challenge);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_domain_patterns() {
|
||||
for pattern in [
|
||||
"*",
|
||||
"*.",
|
||||
"example.*",
|
||||
".example.com",
|
||||
"example..com",
|
||||
"127.0.0.1",
|
||||
"[::1]",
|
||||
"bad_domain.example",
|
||||
"-bad.example",
|
||||
"bad-.example",
|
||||
"éxample.com",
|
||||
] {
|
||||
assert!(
|
||||
DomainPattern::parse(pattern).is_none(),
|
||||
"pattern: {pattern}"
|
||||
);
|
||||
}
|
||||
let long_label = format!("{}.example", "a".repeat(64));
|
||||
assert!(DomainPattern::parse(&long_label).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_salted_hashes_that_verify() {
|
||||
let first = hash_password(PASSWORD).unwrap();
|
||||
@@ -474,6 +751,7 @@ mod tests {
|
||||
action: Action::Allow,
|
||||
users: vec![],
|
||||
networks: vec![],
|
||||
domains: vec![],
|
||||
}],
|
||||
});
|
||||
assert!(matches!(empty, Err(AclError::EmptyRule(_))));
|
||||
|
||||
@@ -94,6 +94,8 @@ pub(crate) struct RuleConfig {
|
||||
pub users: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub networks: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub domains: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
@@ -142,6 +144,34 @@ acl:
|
||||
assert_eq!(config.upstream_timeout, Duration::from_secs(30));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_domain_rules() {
|
||||
let valid = Config::from_yaml(
|
||||
r#"
|
||||
acl:
|
||||
rules:
|
||||
- name: domains
|
||||
action: allow
|
||||
domains: ["example.com", "*.rust-lang.org"]
|
||||
"#,
|
||||
);
|
||||
assert!(valid.is_ok());
|
||||
|
||||
let invalid = Config::from_yaml(
|
||||
r#"
|
||||
acl:
|
||||
rules:
|
||||
- name: invalid-domain
|
||||
action: deny
|
||||
domains: ["example.*"]
|
||||
"#,
|
||||
);
|
||||
assert!(matches!(
|
||||
invalid,
|
||||
Err(ConfigError::Acl(AclError::InvalidDomain { .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_fields_and_zero_timeouts() {
|
||||
let unknown = Config::from_yaml("acl: {}\nunknown: true\n");
|
||||
|
||||
+69
-5
@@ -28,7 +28,7 @@ use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
use crate::{
|
||||
BoxError,
|
||||
acl::{Acl, Decision},
|
||||
acl::{Acl, Decision, DestinationHost},
|
||||
config::Config,
|
||||
metrics::Metrics,
|
||||
telemetry::{HeaderExtractor, HeaderInjector, remove_hop_by_hop_headers},
|
||||
@@ -177,7 +177,16 @@ fn authorize_request(
|
||||
peer: IpAddr,
|
||||
state: &ProxyState,
|
||||
) -> Result<Option<ProxyResponse>, ProxyError> {
|
||||
let outcome = state.acl.authorize(peer, request.headers());
|
||||
let destination = request.uri().authority().and_then(|authority| {
|
||||
if authority.as_str().contains('@') {
|
||||
None
|
||||
} else {
|
||||
DestinationHost::parse(authority.host())
|
||||
}
|
||||
});
|
||||
let outcome = state
|
||||
.acl
|
||||
.authorize(peer, request.headers(), destination.as_ref());
|
||||
|
||||
state
|
||||
.metrics
|
||||
@@ -671,7 +680,10 @@ mod tests {
|
||||
let (mut sender, connection_task) = connect_client(proxy_addr).await;
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(format!("http://{upstream_addr}/hello?q=1"))
|
||||
.uri(format!(
|
||||
"http://localhost:{}/hello?q=1",
|
||||
upstream_addr.port()
|
||||
))
|
||||
.header(HOST, "wrong.invalid")
|
||||
.header(PROXY_AUTHORIZATION, basic_authorization())
|
||||
.body(Full::new(Bytes::new()))
|
||||
@@ -685,7 +697,7 @@ mod tests {
|
||||
upstream_task.abort();
|
||||
let observed = observed.lock().unwrap();
|
||||
assert_eq!(observed.target, "/hello?q=1");
|
||||
assert_eq!(observed.host, upstream_addr.to_string());
|
||||
assert_eq!(observed.host, format!("localhost:{}", upstream_addr.port()));
|
||||
assert!(!observed.forwarded_proxy_authorization);
|
||||
drop(observed);
|
||||
|
||||
@@ -710,7 +722,7 @@ mod tests {
|
||||
let (mut sender, connection_task) = connect_client(proxy_addr).await;
|
||||
let request = Request::builder()
|
||||
.method(Method::CONNECT)
|
||||
.uri(echo_addr.to_string())
|
||||
.uri(format!("localhost:{}", echo_addr.port()))
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
@@ -759,6 +771,56 @@ mod tests {
|
||||
connection_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn denies_blacklisted_http_domain_before_upstream_connection() {
|
||||
let acl = Acl::from_config(AclConfig {
|
||||
default_action: Action::Allow,
|
||||
users: BTreeMap::new(),
|
||||
rules: vec![RuleConfig {
|
||||
name: "blocked-domain".to_owned(),
|
||||
action: Action::Deny,
|
||||
users: vec![],
|
||||
networks: vec![],
|
||||
domains: vec!["blocked.example".to_owned()],
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
let (proxy_addr, proxy_task) = spawn_proxy_with_acl(acl).await;
|
||||
let (mut sender, connection_task) = connect_client(proxy_addr).await;
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("http://blocked.example/")
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let response = sender.send_request(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
response.into_body().collect().await.unwrap();
|
||||
|
||||
proxy_task.abort();
|
||||
connection_task.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn domain_whitelist_rejects_ip_literal_connect() {
|
||||
let echo_listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let echo_addr = echo_listener.local_addr().unwrap();
|
||||
let (proxy_addr, proxy_task) = spawn_proxy_with_acl(localhost_acl()).await;
|
||||
let (mut sender, connection_task) = connect_client(proxy_addr).await;
|
||||
let request = Request::builder()
|
||||
.method(Method::CONNECT)
|
||||
.uri(echo_addr.to_string())
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap();
|
||||
|
||||
let response = sender.send_request(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
response.into_body().collect().await.unwrap();
|
||||
|
||||
proxy_task.abort();
|
||||
connection_task.abort();
|
||||
}
|
||||
|
||||
fn basic_authorization() -> String {
|
||||
format!(
|
||||
"Basic {}",
|
||||
@@ -781,6 +843,7 @@ mod tests {
|
||||
action: Action::Allow,
|
||||
users: vec!["alice".to_owned()],
|
||||
networks: vec![],
|
||||
domains: vec!["localhost".to_owned()],
|
||||
}],
|
||||
})
|
||||
.unwrap()
|
||||
@@ -795,6 +858,7 @@ mod tests {
|
||||
action: Action::Allow,
|
||||
users: vec![],
|
||||
networks: vec!["127.0.0.1/32".to_owned(), "::1/128".to_owned()],
|
||||
domains: vec!["localhost".to_owned()],
|
||||
}],
|
||||
})
|
||||
.unwrap()
|
||||
|
||||
Reference in New Issue
Block a user