use rustls::server::{Accepted, Acceptor};
use std::collections::HashSet;
use std::fmt;
use std::io;
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::sync::Semaphore;
use tokio::sync::broadcast;
use crate::config::{Egress, Subject};
use crate::daemon::egress::{State, Token};
use crate::daemon::server::ACCEPT_RETRY_DELAY;
use crate::machine::{Decoded, Flow, Gateway};
const HTTPS_SCHEME: &str = "https";
const READ_TIMEOUT: Duration = Duration::from_secs(5);
const LOOKUP_TIMEOUT: Duration = Duration::from_secs(5);
const LOOKUPS_MAX: usize = 32;
#[derive(Debug)]
enum ReadError<E> {
Short,
Io(io::Error),
Refused(E),
}
impl<E: fmt::Display> fmt::Display for ReadError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadError::Short => write!(f, "short read"),
ReadError::Io(error) => write!(f, "{error}"),
ReadError::Refused(error) => write!(f, "{error}"),
}
}
}
impl<E> From<io::Error> for ReadError<E> {
fn from(error: io::Error) -> Self {
if error.kind() == io::ErrorKind::UnexpectedEof {
ReadError::Short
} else {
ReadError::Io(error)
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Verdict {
Allow,
Deny(Reason),
}
#[derive(Debug, PartialEq, Eq)]
enum Reason {
Loopback,
GuestLink,
HostAddress,
BuiltIn,
Listed,
Unnamed,
NotAllowed,
}
impl fmt::Display for Reason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let words = match self {
Reason::Loopback => "loopback",
Reason::GuestLink => "guest link",
Reason::HostAddress => "host address",
Reason::BuiltIn => "built-in deny",
Reason::Listed => "listed in deny",
Reason::Unnamed => "no name to check against deny",
Reason::NotAllowed => "no list allows it",
};
write!(f, "{words}")
}
}
#[derive(Debug, PartialEq, Eq)]
enum Unbound {
Elsewhere(IpAddr),
Late,
Busy,
}
impl fmt::Display for Unbound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Unbound::Elsewhere(ip) => write!(f, "does not resolve to {ip}"),
Unbound::Late => write!(f, "did not resolve within {} s", LOOKUP_TIMEOUT.as_secs()),
Unbound::Busy => write!(f, "was not looked up: too many lookups running"),
}
}
}
pub(super) async fn serve<G: Gateway>(state: Arc<State>, log: broadcast::Sender<String>) {
let listener = state.listener().and_then(|listener| {
listener.set_nonblocking(true)?;
TcpListener::from_std(listener)
});
let listener = match listener {
Ok(listener) => listener,
Err(error) => {
drop(log.send(format!("egress: listener failed: {error}")));
return;
}
};
let logged = Arc::new(Mutex::new(HashSet::new()));
let lookups = Arc::new(Semaphore::new(LOOKUPS_MAX));
loop {
match listener.accept().await {
Ok((stream, peer)) => {
tokio::spawn(handle_flow::<G, _>(
stream,
peer,
Arc::clone(&state),
log.clone(),
Arc::clone(&logged),
Arc::clone(&lookups),
));
}
Err(error) => {
drop(log.send(format!("egress: accept failed: {error}")));
tokio::time::sleep(ACCEPT_RETRY_DELAY).await;
}
}
}
}
async fn handle_flow<G: Gateway, S: AsyncRead + AsyncWrite + Unpin>(
mut stream: S,
peer: SocketAddr,
state: Arc<State>,
log: broadcast::Sender<String>,
logged: Arc<Mutex<HashSet<IpAddr>>>,
lookups: Arc<Semaphore>,
) {
let flow = tokio::time::timeout(READ_TIMEOUT, read_flow::<G>(&mut stream))
.await
.unwrap_or(Err(ReadError::Short));
let flow = match flow {
Ok(flow) => flow,
Err(error) => {
if logged
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(peer.ip())
{
drop(log.send(format!("egress: denied {peer}: bad preamble, {error}")));
}
drop(stream.write_all(&[G::DENY]).await);
return;
}
};
let destination = flow.destination;
let Some((machine, egress)) = state.lookup(&Token(flow.token)) else {
if logged
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(destination.ip())
{
drop(log.send(format!("egress: denied {destination}: unknown token")));
}
drop(stream.write_all(&[G::DENY]).await);
return;
};
let name = match claimed_name(&flow.opening) {
Some(claimed) => match bind_name(&claimed, destination, &lookups).await {
Ok(name) => name,
Err(unbound) => {
drop(log.send(format!("egress: {claimed} {unbound}")));
None
}
},
None => None,
};
let subject = Subject {
address: destination.ip(),
name,
};
let reply = match evaluate::<G>(&egress, &subject) {
Verdict::Allow => G::ALLOW,
Verdict::Deny(reason) => {
let target = match &subject.name {
Some(name) => format!("{HTTPS_SCHEME}://{name} ({destination})"),
None => destination.to_string(),
};
drop(log.send(format!("egress: denied {machine} to {target}: {reason}")));
G::DENY
}
};
drop(stream.write_all(&[reply]).await);
}
async fn read_flow<G: Gateway>(
stream: &mut (impl AsyncRead + Unpin),
) -> Result<Flow, ReadError<G::FlowError>> {
let mut bytes = Vec::new();
loop {
match G::decode_flow(&bytes).map_err(ReadError::Refused)? {
Decoded::Complete(flow) => return Ok(flow),
Decoded::Partial(len) => {
let mut rest = vec![0; len.saturating_sub(bytes.len()).max(1)];
stream.read_exact(&mut rest).await?;
bytes.extend(rest);
}
}
}
}
pub(super) fn claimed_name(opening: &[u8]) -> Option<String> {
let accepted = client_hello(opening)?;
accepted.client_hello().server_name().map(str::to_owned)
}
fn client_hello(opening: &[u8]) -> Option<Accepted> {
let mut acceptor = Acceptor::default();
let mut tls_bytes = opening;
while let Ok(1..) = acceptor.read_tls(&mut tls_bytes) {
if let Ok(Some(accepted)) = acceptor.accept() {
return Some(accepted);
}
}
None
}
async fn bind_name(
claimed: &str,
destination: SocketAddr,
lookups: &Arc<Semaphore>,
) -> Result<Option<String>, Unbound> {
let Some(normalized) = Subject::normalize_name(claimed) else {
return Ok(None);
};
let ip = destination.ip();
let host = (normalized.clone(), destination.port());
let binds = resolve(lookups, move || {
host.to_socket_addrs()
.is_ok_and(|mut answers| answers.any(|answer| answer.ip() == ip))
})
.await?;
if binds {
Ok(Some(normalized))
} else {
Err(Unbound::Elsewhere(ip))
}
}
async fn resolve(
lookups: &Arc<Semaphore>,
lookup: impl FnOnce() -> bool + Send + 'static,
) -> Result<bool, Unbound> {
let Ok(permit) = Arc::clone(lookups).try_acquire_owned() else {
return Err(Unbound::Busy);
};
let thread = tokio::task::spawn_blocking(move || {
let answer = lookup();
drop(permit);
answer
});
tokio::time::timeout(LOOKUP_TIMEOUT, thread)
.await
.ok()
.and_then(Result::ok)
.ok_or(Unbound::Late)
}
fn evaluate<G: Gateway>(egress: &Egress, subject: &Subject) -> Verdict {
let address = subject.address.to_canonical();
if let Some(reason) = own_address::<G>(address) {
return Verdict::Deny(reason);
}
if Egress::built_in_denies(address) {
return Verdict::Deny(Reason::BuiltIn);
}
if egress.denies(subject) {
return Verdict::Deny(Reason::Listed);
}
if egress.allows(subject) {
return Verdict::Allow;
}
if subject.name.is_none() && egress.has_wildcard() && egress.has_name_deny() {
return Verdict::Deny(Reason::Unnamed);
}
Verdict::Deny(Reason::NotAllowed)
}
fn own_address<G: Gateway>(ip: IpAddr) -> Option<Reason> {
if ip.is_loopback() || ip.is_unspecified() {
Some(Reason::Loopback)
} else if G::is_guest_link(ip) {
Some(Reason::GuestLink)
} else if G::is_host_address(ip) {
Some(Reason::HostAddress)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Pattern;
use crate::daemon::egress::Token;
use crate::daemon::machines::{Marker, MarkerEgress};
use crate::machine::Spec;
use std::cell::Cell;
use std::net::Ipv6Addr;
use tempfile::TempDir;
use tokio::io::{AsyncWriteExt, duplex};
const CLIENT_HELLO: &[u8] = include_bytes!("decider/testdata/client_hello_example_com.bin");
struct FakeGateway;
impl Gateway for FakeGateway {
type FlowError = &'static str;
const ALLOW: u8 = b'y';
const DENY: u8 = b'n';
fn decode_flow(bytes: &[u8]) -> Result<Decoded, &'static str> {
let Some(head) = bytes.get(..39) else {
return Ok(Decoded::Partial(39));
};
let length = usize::from(head[38]);
if length > 128 {
return Err("more than 128 opening bytes");
}
let Some(opening) = bytes.get(39..39 + length) else {
return Ok(Decoded::Partial(39 + length));
};
Ok(Decoded::Complete(Flow {
token: head[..32].try_into().unwrap(),
destination: SocketAddr::from((
[head[32], head[33], head[34], head[35]],
u16::from_be_bytes([head[36], head[37]]),
)),
opening: opening.to_vec(),
}))
}
fn is_guest_link(ip: IpAddr) -> bool {
ip == IpAddr::from([198, 51, 100, 1])
}
fn is_host_address(ip: IpAddr) -> bool {
ip == IpAddr::from([198, 51, 100, 2])
}
}
thread_local! {
static QUESTIONS: Cell<u32> = const { Cell::new(0) };
}
struct StubbornGateway;
impl Gateway for StubbornGateway {
type FlowError = &'static str;
const ALLOW: u8 = b'y';
const DENY: u8 = b'n';
fn decode_flow(bytes: &[u8]) -> Result<Decoded, &'static str> {
QUESTIONS.set(QUESTIONS.get() + 1);
if QUESTIONS.get() > 10 {
return Err("read_flow asked more than ten times");
}
match bytes {
[] | [b'=', _, _, _] => Ok(Decoded::Partial(4)),
[b'<', _, _, _] => Ok(Decoded::Partial(2)),
_ => Err("refused"),
}
}
fn is_guest_link(_: IpAddr) -> bool {
false
}
fn is_host_address(_: IpAddr) -> bool {
false
}
}
#[tokio::test]
async fn read_flow_reads_what_the_gateway_asks_for() {
let mut bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, b"GET");
bytes.extend(b"rest");
let mut stream = &bytes[..];
let flow = read_flow::<FakeGateway>(&mut stream).await.unwrap();
assert_eq!(flow.token, [0xaa; 32]);
assert_eq!(flow.destination, SocketAddr::from(([192, 0, 2, 1], 443)));
assert_eq!(flow.opening, b"GET");
assert_eq!(stream, b"rest");
}
#[tokio::test]
async fn short_and_refused_requests_are_errors() {
let bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, b"GET");
let Err(error) = read_flow::<FakeGateway>(&mut &bytes[..bytes.len() - 1]).await else {
panic!("read a request cut short");
};
assert_eq!(error.to_string(), "short read");
let mut bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, &[]);
*bytes.last_mut().unwrap() = 129;
bytes.extend([b'x'; 129]);
let mut stream = &bytes[..];
let Err(error) = read_flow::<FakeGateway>(&mut stream).await else {
panic!("read a request over the gateway's limit");
};
assert_eq!(error.to_string(), "more than 128 opening bytes");
assert_eq!(stream.len(), 129);
}
#[tokio::test]
async fn a_gateway_asking_for_no_more_bytes_still_ends_the_read() {
for bytes in [b"====rest", b"<<<<rest"] {
assert!(
matches!(
read_flow::<StubbornGateway>(&mut &bytes[..]).await,
Err(ReadError::Refused("refused"))
),
"{}",
bytes.escape_ascii()
);
}
}
#[test]
fn sni_is_read_from_the_client_hello() {
assert_eq!(claimed_name(CLIENT_HELLO), Some("example.com".to_string()));
assert_eq!(claimed_name(&CLIENT_HELLO[..20]), None);
assert_eq!(
claimed_name(&[0x16, 0x03, 0x01, 0x00, 0x05, 1, 2, 3, 4, 5]),
None
);
}
#[test]
fn a_hello_in_many_small_records_claims_its_sni() {
let mut records = Vec::new();
for piece in CLIENT_HELLO[5..].chunks(2) {
records.extend([0x16, 0x03, 0x01, 0x00, u8::try_from(piece.len()).unwrap()]);
records.extend(piece);
}
assert_eq!(records.len(), 5389);
assert_eq!(claimed_name(&records), Some("example.com".to_string()));
}
#[test]
fn a_plain_http_request_claims_no_name() {
for head in [
"GET / HTTP/1.1\r\nHost: api.github.com\r\n\r\n",
"GET http://api.github.com/ HTTP/1.1\r\nHost: api.github.com\r\n\r\n",
"CONNECT api.github.com:443 HTTP/1.1\r\nHost: api.github.com:443\r\n\r\n",
] {
assert_eq!(claimed_name(head.as_bytes()), None, "{head:?}");
}
}
#[tokio::test]
async fn an_ip_literal_claim_is_no_name() {
let lookups = Arc::new(Semaphore::new(1));
assert_eq!(
bind_name(
"169.254.169.254",
SocketAddr::from(([169, 254, 169, 254], 80)),
&lookups
)
.await,
Ok(None)
);
assert_eq!(
bind_name(
"[::1]",
SocketAddr::from((Ipv6Addr::LOCALHOST, 80)),
&lookups
)
.await,
Ok(None)
);
}
#[tokio::test]
async fn bind_name_requires_the_dialed_address_among_the_answers() {
let lookups = Arc::new(Semaphore::new(1));
let loopback = SocketAddr::from(([127, 0, 0, 1], 443));
let elsewhere = SocketAddr::from(([192, 0, 2, 1], 443));
assert_eq!(
bind_name("localhost", loopback, &lookups).await,
Ok(Some("localhost".to_string()))
);
assert_eq!(
bind_name("LOCALHOST.", loopback, &lookups).await,
Ok(Some("localhost".to_string()))
);
assert_eq!(
bind_name("localhost", elsewhere, &lookups).await,
Err(Unbound::Elsewhere(elsewhere.ip()))
);
assert_eq!(
bind_name("nope.invalid", elsewhere, &lookups).await,
Err(Unbound::Elsewhere(elsewhere.ip()))
);
}
#[tokio::test]
async fn a_lookup_over_the_cap_is_not_made() {
let busy = bind_name(
"localhost",
SocketAddr::from(([127, 0, 0, 1], 443)),
&Arc::new(Semaphore::new(0)),
)
.await;
assert_eq!(busy, Err(Unbound::Busy));
assert_eq!(
busy.unwrap_err().to_string(),
"was not looked up: too many lookups running"
);
}
#[tokio::test(start_paused = true)]
async fn a_stuck_lookup_is_late_and_keeps_its_permit_until_it_ends() {
let lookups = Arc::new(Semaphore::new(1));
let (release, released) = std::sync::mpsc::channel::<()>();
let lookup = move || released.recv_timeout(Duration::from_secs(10)).is_ok();
let (late, ()) = tokio::join!(
resolve(&lookups, lookup),
tokio::time::advance(Duration::from_secs(5))
);
assert_eq!(late, Err(Unbound::Late));
assert_eq!(late.unwrap_err().to_string(), "did not resolve within 5 s");
assert!(lookups.try_acquire().is_err());
release.send(()).unwrap();
assert!(
tokio::time::timeout(Duration::from_secs(10), lookups.acquire())
.await
.is_ok()
);
}
#[tokio::test]
async fn a_name_that_resolves_elsewhere_is_dropped() {
let destination = SocketAddr::from(([203, 0, 113, 9], 443));
assert_eq!(
bind_name("localhost", destination, &Arc::new(Semaphore::new(1))).await,
Err(Unbound::Elsewhere(destination.ip()))
);
let subject = Subject {
address: destination.ip(),
name: None,
};
assert_eq!(
evaluate::<FakeGateway>(&egress(&["localhost"], &[]), &subject),
Verdict::Deny(Reason::NotAllowed)
);
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "the length is the table's rows, one per case, grouped by step"
)]
fn evaluation_order() {
use Reason::{BuiltIn, GuestLink, HostAddress, Listed, Loopback, NotAllowed, Unnamed};
use Verdict::{Allow, Deny};
type Case<'a> = (
&'a [&'a str],
&'a [&'a str],
&'a str,
Option<&'a str>,
Verdict,
);
let cases: &[Case<'_>] = &[
(&[], &[], "127.0.0.1", Some("localhost"), Deny(Loopback)),
(&["127.0.0.1"], &[], "127.0.0.1", None, Deny(Loopback)),
(&["*"], &[], "::ffff:127.0.0.1", None, Deny(Loopback)),
(&["*"], &[], "0.0.0.0", None, Deny(Loopback)),
(&["*"], &[], "::", None, Deny(Loopback)),
(&[], &[], "198.51.100.1", None, Deny(GuestLink)),
(&["*"], &[], "198.51.100.1", None, Deny(GuestLink)),
(&["*"], &[], "::ffff:198.51.100.1", None, Deny(GuestLink)),
(
&["198.51.100.1"],
&[],
"198.51.100.1",
None,
Deny(GuestLink),
),
(&["*"], &[], "198.51.100.2", None, Deny(HostAddress)),
(&["*"], &[], "169.254.169.254", None, Deny(BuiltIn)),
(
&["*.example"],
&[],
"93.184.216.34",
Some("a.example"),
Allow,
),
(
&["*.example"],
&[],
"93.184.216.34",
Some("example.com"),
Deny(NotAllowed),
),
(&["*.example"], &[], "93.184.216.34", None, Deny(NotAllowed)),
(
&["*.example", "93.184.216.34"],
&[],
"93.184.216.34",
None,
Allow,
),
(&["*"], &[], "93.184.216.34", None, Allow),
(&["*", "api.github.com"], &[], "93.184.216.34", None, Allow),
(
&[],
&[],
"93.184.216.34",
Some("example.com"),
Deny(NotAllowed),
),
(&[], &[], "93.184.216.34", None, Deny(NotAllowed)),
(
&["*.example"],
&["evil.example"],
"93.184.216.34",
Some("evil.example"),
Deny(Listed),
),
(
&["*"],
&["evil.example"],
"93.184.216.34",
Some("a.example"),
Allow,
),
(
&["*"],
&["evil.example"],
"93.184.216.34",
None,
Deny(Unnamed),
),
(
&["*", "93.184.216.34"],
&["evil.example"],
"93.184.216.34",
None,
Allow,
),
(
&["*"],
&["93.184.216.34"],
"93.184.216.34",
Some("a.example"),
Deny(Listed),
),
(&["*"], &["93.184.216.34"], "203.0.113.9", None, Allow),
];
for (allow, deny, destination, name, verdict) in cases {
let subject = Subject {
address: destination.parse().unwrap(),
name: name.map(str::to_owned),
};
assert_eq!(
&evaluate::<FakeGateway>(&egress(allow, deny), &subject),
verdict,
"allow {allow:?}, deny {deny:?}, {destination}, {name:?}"
);
}
}
#[test]
fn reasons_display_their_log_words() {
for (reason, words) in [
(Reason::Loopback, "loopback"),
(Reason::GuestLink, "guest link"),
(Reason::HostAddress, "host address"),
(Reason::BuiltIn, "built-in deny"),
(Reason::Listed, "listed in deny"),
(Reason::Unnamed, "no name to check against deny"),
(Reason::NotAllowed, "no list allows it"),
] {
assert_eq!(reason.to_string(), words);
}
}
#[tokio::test(start_paused = true)]
async fn a_silent_client_is_dropped_at_the_deadline() {
let (_dir, state) = alpha(&["*"], &[]);
let (log, mut sent) = broadcast::channel(16);
assert_eq!(answer(&state, &log, &Arc::default(), &[]).await, b'n');
assert_eq!(
lines(&mut sent),
["egress: denied 127.0.0.1:9: bad preamble, short read"]
);
}
#[tokio::test]
async fn a_bad_preamble_is_logged_once_per_peer_address() {
let (_dir, state) = alpha(&["*"], &[]);
let (log, mut sent) = broadcast::channel(16);
let logged = Arc::default();
let mut bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, &[]);
*bytes.last_mut().unwrap() = 129;
assert_eq!(answer(&state, &log, &logged, &bytes).await, b'n');
let next_port = SocketAddr::from(([127, 0, 0, 1], 10));
assert_eq!(
answer_from(next_port, &state, &log, &logged, &bytes).await,
b'n'
);
assert_eq!(
lines(&mut sent),
["egress: denied 127.0.0.1:9: bad preamble, more than 128 opening bytes"]
);
}
#[tokio::test]
async fn an_unknown_token_is_denied_and_logged_once_per_destination() {
let (_dir, state) = alpha(&["*"], &[]);
let (log, mut sent) = broadcast::channel(16);
let logged = Arc::default();
let bytes = request(&[0xbb; 32], [192, 0, 2, 1], 443, &[]);
assert_eq!(answer(&state, &log, &logged, &bytes).await, b'n');
assert_eq!(answer(&state, &log, &logged, &bytes).await, b'n');
assert_eq!(
lines(&mut sent),
["egress: denied 192.0.2.1:443: unknown token"]
);
}
#[tokio::test]
async fn an_allowed_flow_is_not_logged() {
let (_dir, state) = alpha(&["*"], &[]);
let (log, mut sent) = broadcast::channel(16);
let bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, &[]);
assert_eq!(answer(&state, &log, &Arc::default(), &bytes).await, b'y');
assert_eq!(lines(&mut sent), Vec::<String>::new());
}
#[tokio::test]
async fn a_dropped_name_is_logged_then_judged() {
let (_dir, state) = alpha(&["*"], &[]);
let (log, mut sent) = broadcast::channel(16);
let bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, &hello("other.invalid"));
assert_eq!(answer(&state, &log, &Arc::default(), &bytes).await, b'y');
assert_eq!(
lines(&mut sent),
["egress: other.invalid does not resolve to 192.0.2.1"]
);
}
#[tokio::test]
async fn a_name_deny_turns_away_a_dropped_name() {
let (_dir, state) = alpha(&["*"], &["evil.example"]);
let (log, mut sent) = broadcast::channel(16);
let bytes = request(&[0xaa; 32], [192, 0, 2, 1], 443, &hello("other.invalid"));
assert_eq!(answer(&state, &log, &Arc::default(), &bytes).await, b'n');
assert_eq!(
lines(&mut sent),
[
"egress: other.invalid does not resolve to 192.0.2.1",
"egress: denied alpha to 192.0.2.1:443: no name to check against deny",
]
);
}
#[tokio::test]
async fn a_named_deny_names_the_url() {
let (_dir, state) = alpha(&[], &[]);
let (log, mut sent) = broadcast::channel(16);
let bytes = request(&[0xaa; 32], [127, 0, 0, 1], 443, &hello("localhost"));
assert_eq!(answer(&state, &log, &Arc::default(), &bytes).await, b'n');
assert_eq!(
lines(&mut sent),
["egress: denied alpha to https://localhost (127.0.0.1:443): loopback"]
);
}
fn request(token: &[u8; 32], ip: [u8; 4], port: u16, opening: &[u8]) -> Vec<u8> {
let mut bytes = token.to_vec();
bytes.extend(ip);
bytes.extend(port.to_be_bytes());
bytes.push(u8::try_from(opening.len()).unwrap());
bytes.extend(opening);
bytes
}
fn hello(name: &str) -> Vec<u8> {
let name = name.as_bytes();
let name_len = u16::try_from(name.len()).unwrap();
let mut body = vec![0x03, 0x03];
body.extend([0; 32]);
body.extend([0x00, 0x00, 0x02, 0x13, 0x01, 0x01, 0x00]);
body.extend((name_len + 17).to_be_bytes());
body.extend([0x00, 0x00]);
body.extend((name_len + 5).to_be_bytes());
body.extend((name_len + 3).to_be_bytes());
body.push(0x00);
body.extend(name_len.to_be_bytes());
body.extend(name);
body.extend([0x00, 0x0d, 0x00, 0x04, 0x00, 0x02, 0x08, 0x07]);
let body_len = u16::try_from(body.len()).unwrap();
let mut record = vec![0x16, 0x03, 0x01];
record.extend((body_len + 4).to_be_bytes());
record.extend([0x01, 0x00]);
record.extend(body_len.to_be_bytes());
record.extend(body);
record
}
fn egress(allow: &[&str], deny: &[&str]) -> Egress {
let parse = |entries: &[&str]| -> Vec<Pattern> {
entries
.iter()
.map(|entry| Pattern::parse(entry).unwrap())
.collect()
};
Egress {
allow: parse(allow),
deny: parse(deny),
}
}
fn alpha(allow: &[&str], deny: &[&str]) -> (TempDir, Arc<State>) {
let tmp = tempfile::tempdir().unwrap();
let uid = rustix::process::geteuid().as_raw();
let dir = crate::daemon::state::prepare_in(&tmp.path().join("state"), uid).unwrap();
let state = State::bind(&dir).unwrap();
state.register(
"alpha",
&Marker {
egress: MarkerEgress {
token: Token([0xaa; 32]),
},
spec: Spec {
egress: egress(allow, deny),
..Spec::default()
},
},
);
(tmp, Arc::new(state))
}
async fn answer(
state: &Arc<State>,
log: &broadcast::Sender<String>,
logged: &Arc<Mutex<HashSet<IpAddr>>>,
bytes: &[u8],
) -> u8 {
answer_from(
SocketAddr::from(([127, 0, 0, 1], 9)),
state,
log,
logged,
bytes,
)
.await
}
async fn answer_from(
peer: SocketAddr,
state: &Arc<State>,
log: &broadcast::Sender<String>,
logged: &Arc<Mutex<HashSet<IpAddr>>>,
bytes: &[u8],
) -> u8 {
let (mut gateway, decider) = duplex(1024);
gateway.write_all(bytes).await.unwrap();
handle_flow::<FakeGateway, _>(
decider,
peer,
Arc::clone(state),
log.clone(),
Arc::clone(logged),
Arc::new(Semaphore::new(1)),
)
.await;
let mut answer = [0];
gateway.read_exact(&mut answer).await.unwrap();
answer[0]
}
fn lines(log: &mut broadcast::Receiver<String>) -> Vec<String> {
let mut lines = Vec::new();
while let Ok(line) = log.try_recv() {
lines.push(line);
}
lines
}
}