mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-09 05:19:35 -10:00
Compare commits
6
Commits
clap
...
windows/crl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c781404f | ||
|
|
0ba2e23032 | ||
|
|
f500e4a42a | ||
|
|
b2e036f7c6 | ||
|
|
7f9e257a38 | ||
|
|
098dea7cc0 |
No files matched your search
Generated
+108
-7
@@ -45,6 +45,56 @@ dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
@@ -210,12 +260,6 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bpaf"
|
||||
version = "0.9.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597e81e56404b7a3e26ef7b449f82a4b7a661ae76c3cac692b42d6bad06a0fbe"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.4"
|
||||
@@ -313,6 +357,33 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
@@ -322,6 +393,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.8"
|
||||
@@ -1100,6 +1177,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
@@ -1562,6 +1645,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
@@ -3004,7 +3093,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
name = "strix-firewall"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bpaf",
|
||||
"clap",
|
||||
"core-foundation",
|
||||
"directories",
|
||||
"percent-encoding",
|
||||
@@ -3024,6 +3113,12 @@ dependencies = [
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -3433,6 +3528,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.26.1"
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ name = "managers"
|
||||
path = "tests/managers.rs"
|
||||
|
||||
[dependencies]
|
||||
bpaf = "0.9.28"
|
||||
clap = "4.6.6"
|
||||
directories = "6.0.0"
|
||||
percent-encoding = "2.3.2"
|
||||
rama = { version = "0.4.0", features = ["boring", "http-full", "ring", "rustls"] }
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
use crate::http::{blocked, https_url, text};
|
||||
use crate::{policies, telemetry};
|
||||
use crate::{Firewall, FirewallConfig};
|
||||
use rama::dns::client::DnsConnectorLayer;
|
||||
use rama::http::body::util::BodyExt;
|
||||
use rama::http::client::EasyHttpWebClient;
|
||||
use rama::http::header::CONTENT_LENGTH;
|
||||
use rama::http::layer::decompression::DecompressionLayer;
|
||||
use rama::http::layer::remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer};
|
||||
use rama::http::layer::upgrade::{EagerHttpProxyConnector, UpgradeLayer};
|
||||
use rama::http::server::HttpServer;
|
||||
use rama::http::{Body, Method, Request, Response};
|
||||
use rama::matcher::match_fn;
|
||||
use rama::net::proxy::IoForwardService;
|
||||
use rama::rt::Executor;
|
||||
use rama::service::service_fn;
|
||||
use rama::tcp::client::service::TcpConnector;
|
||||
use rama::tcp::server::TcpListener;
|
||||
use rama::{Layer, Service};
|
||||
use std::convert::Infallible;
|
||||
@@ -31,8 +36,26 @@ pub fn spawn(port: u16, config: FirewallConfig) -> std::io::Result<u16> {
|
||||
Ok(listener) => listener,
|
||||
Err(e) => return error!(error = %e, "forward proxy could not take over the socket"),
|
||||
};
|
||||
// Tunnels skip screening, so a registry's CONNECT falls through to `handle`, which refuses it.
|
||||
let hosts = firewall.hosts();
|
||||
let tunnel = UpgradeLayer::new(
|
||||
exec.clone(),
|
||||
match_fn(move |req: &Request| {
|
||||
let host = req.uri().host().map(|host| host.to_str());
|
||||
req.method() == Method::CONNECT
|
||||
&& host.is_some_and(|host| !hosts.contains(&host.as_ref()))
|
||||
}),
|
||||
EagerHttpProxyConnector::new(
|
||||
DnsConnectorLayer::new().into_layer(TcpConnector::new()),
|
||||
IoForwardService::default(),
|
||||
),
|
||||
);
|
||||
let server = HttpServer::auto(exec).service(
|
||||
(RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop())
|
||||
(
|
||||
RemoveResponseHeaderLayer::hop_by_hop(),
|
||||
RemoveRequestHeaderLayer::hop_by_hop(),
|
||||
tunnel,
|
||||
)
|
||||
.into_layer(service_fn(move |req: Request| {
|
||||
let firewall = Arc::clone(&firewall);
|
||||
async move { handle(req, &firewall).await }
|
||||
|
||||
@@ -41,8 +41,10 @@ pub struct Manager {
|
||||
pub static MANAGERS: &[&Manager] =
|
||||
&[&NPM, &PIP, &PIP3, &CARGO, &GO, &GEM, &UV, &PDM, &PIPENV, &PNPM, &BUN];
|
||||
|
||||
pub fn variables(proxy: &str, manager: &Manager) -> Vec<(String, String)> {
|
||||
manager.env.iter().map(|(key, value)| (key.to_string(), expand(value, proxy))).collect()
|
||||
/// Every manager's variables, so whichever the command starts is pointed at the firewall.
|
||||
pub fn variables(proxy: &str) -> Vec<(String, String)> {
|
||||
let env = MANAGERS.iter().flat_map(|manager| manager.env);
|
||||
env.map(|(key, value)| (key.to_string(), expand(value, proxy))).collect()
|
||||
}
|
||||
|
||||
pub fn arguments(proxy: &str, manager: &Manager, typed: &[String]) -> Vec<String> {
|
||||
|
||||
@@ -9,6 +9,8 @@ pub static PIPENV: Manager = Manager {
|
||||
("http_proxy", PROXY),
|
||||
("https_proxy", PROXY),
|
||||
("PIPENV_PYPI_MIRROR", PYPI_SIMPLE),
|
||||
// pipenv installs through pip, which skips a plaintext index on a host it doesn't trust.
|
||||
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
||||
],
|
||||
args: &[],
|
||||
};
|
||||
+19
-12
@@ -9,30 +9,37 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub struct Invocation {
|
||||
pub manager: &'static Manager,
|
||||
pub program: String,
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl Invocation {
|
||||
pub fn new(manager: &'static Manager, args: Vec<String>) -> Self {
|
||||
Invocation { manager, args }
|
||||
pub fn new(program: String, args: Vec<String>) -> Self {
|
||||
Invocation { program, args }
|
||||
}
|
||||
|
||||
/// What the program is run with: the typed arguments plus any its manager needs.
|
||||
pub fn arguments(&self, proxy: &str) -> Vec<String> {
|
||||
let mut args = self.args.clone();
|
||||
let Some(manager) = MANAGERS.iter().find(|manager| manager.command == self.program) else {
|
||||
return args;
|
||||
};
|
||||
let ours = managers::arguments(proxy, manager, &args);
|
||||
// Anything after `--` belongs to another program, so ours go before it.
|
||||
let at = args.iter().position(|arg| arg == "--").unwrap_or(args.len());
|
||||
args.splice(at..at, ours);
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::Result<i32> {
|
||||
let proxy = crate::local_url(port, "");
|
||||
let mut args = invocation.args.clone();
|
||||
let ours = managers::arguments(&proxy, invocation.manager, &args);
|
||||
// Anything after `--` belongs to another program, so ours go before it.
|
||||
let at = args.iter().position(|arg| arg == "--").unwrap_or(args.len());
|
||||
args.splice(at..at, ours);
|
||||
|
||||
let mut command = Command::new(resolve(invocation.manager.command)?);
|
||||
command.args(args);
|
||||
let mut command = Command::new(resolve(&invocation.program)?);
|
||||
command.args(invocation.arguments(&proxy));
|
||||
if let Some(dir) = dir {
|
||||
command.current_dir(dir);
|
||||
}
|
||||
for (key, value) in managers::variables(&proxy, invocation.manager) {
|
||||
for (key, value) in managers::variables(&proxy) {
|
||||
command.env(key, value);
|
||||
}
|
||||
Ok(command.status()?.code().unwrap_or(1))
|
||||
|
||||
@@ -80,6 +80,18 @@ impl FirewallConfig {
|
||||
pub fn local_url(&self, path: &str) -> String {
|
||||
crate::local_url(self.port, path)
|
||||
}
|
||||
|
||||
/// Without building a `Firewall`, which needs a runtime.
|
||||
pub fn hosts(&self) -> Vec<&'static str> {
|
||||
let mut hosts: Vec<&'static str> = Vec::new();
|
||||
for registry in &self.registries {
|
||||
let host = registry.host();
|
||||
if !hosts.contains(&host) {
|
||||
hosts.push(host);
|
||||
}
|
||||
}
|
||||
hosts
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path() -> Option<PathBuf> {
|
||||
|
||||
@@ -40,14 +40,7 @@ impl Firewall {
|
||||
}
|
||||
|
||||
pub fn hosts(&self) -> Vec<&'static str> {
|
||||
let mut hosts: Vec<&'static str> = Vec::new();
|
||||
for registry in &self.config.registries {
|
||||
let host = registry.host();
|
||||
if !hosts.contains(&host) {
|
||||
hosts.push(host);
|
||||
}
|
||||
}
|
||||
hosts
|
||||
self.config.hosts()
|
||||
}
|
||||
|
||||
pub fn is_index_url(&self, url: &str) -> bool {
|
||||
|
||||
+62
-52
@@ -1,64 +1,52 @@
|
||||
use bpaf::{any, choice, pure, OptionParser, Parser};
|
||||
use firewall::aliasing::{self, Invocation, Manager, MANAGERS};
|
||||
use clap::{Arg, Command};
|
||||
use firewall::aliasing::{self, Invocation, MANAGERS};
|
||||
use firewall::{config, proxy, telemetry, FirewallConfig};
|
||||
use std::io::ErrorKind;
|
||||
use std::path::Path;
|
||||
use std::process::exit;
|
||||
use tracing::{error, info};
|
||||
|
||||
enum SubCommand {
|
||||
Serve,
|
||||
Run(Invocation),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match options().run() {
|
||||
SubCommand::Serve => {
|
||||
firewall::init_logging("info");
|
||||
serve()
|
||||
}
|
||||
SubCommand::Run(invocation) => {
|
||||
firewall::init_logging("warn");
|
||||
run(invocation)
|
||||
}
|
||||
let matches = options().get_matches();
|
||||
if matches.subcommand_matches("serve").is_some() {
|
||||
firewall::init_logging("info");
|
||||
serve()
|
||||
}
|
||||
let mut command = matches.get_many::<String>("COMMAND").unwrap().cloned();
|
||||
let program = command.next().unwrap();
|
||||
firewall::init_logging("warn");
|
||||
run(Invocation::new(program, command.collect()))
|
||||
}
|
||||
|
||||
fn options() -> OptionParser<SubCommand> {
|
||||
fn options() -> Command {
|
||||
let program = program_name();
|
||||
let managers: Vec<_> = MANAGERS.iter().map(|manager| manager.command).collect();
|
||||
let footer = format!(
|
||||
"Run a package manager through {program} to screen what it installs:\n \
|
||||
{program} npm install left-pad\n \n \
|
||||
Alias it to screen every install without changing how you type:\n \
|
||||
alias npm='{program} npm'"
|
||||
"Run a command through {program} to screen what it installs:\n \
|
||||
{program} npm install left-pad\n\n\
|
||||
Alias it to screen every install without changing how you type:\n \
|
||||
{}\n\n\
|
||||
Supported package managers: {}",
|
||||
alias(&program),
|
||||
managers.join(", ")
|
||||
);
|
||||
let serve = pure(())
|
||||
.map(|()| SubCommand::Serve)
|
||||
.to_options()
|
||||
.descr("Serve a local registry for package managers configured to use it")
|
||||
.command("serve");
|
||||
|
||||
let mut commands: Vec<_> = MANAGERS
|
||||
.iter()
|
||||
.map(|manager| package(manager, &program).boxed())
|
||||
.collect();
|
||||
commands.push(serve.boxed());
|
||||
choice(commands)
|
||||
.to_options()
|
||||
.descr("Screens package downloads before they reach your machine")
|
||||
.footer(Box::leak(footer.into_boxed_str()) as &str)
|
||||
.fallback_to_usage()
|
||||
}
|
||||
|
||||
fn package(manager: &'static Manager, program: &str) -> impl Parser<SubCommand> {
|
||||
let name = manager.command;
|
||||
let descr = format!("Run {name} with downloads screened: {program} {name} ARGS...");
|
||||
any::<String, String, _>("ARGS", Some)
|
||||
.many()
|
||||
.map(move |args| SubCommand::Run(Invocation::new(manager, args)))
|
||||
.to_options()
|
||||
.descr(Box::leak(descr.into_boxed_str()) as &str)
|
||||
.command(name)
|
||||
Command::new("firewall")
|
||||
.bin_name(&program)
|
||||
.about("Screens package downloads before they reach your machine")
|
||||
.after_help(footer)
|
||||
.arg_required_else_help(true)
|
||||
.args_conflicts_with_subcommands(true)
|
||||
.subcommand(
|
||||
Command::new("serve")
|
||||
.about("Serve a local registry for package managers configured to use it"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("COMMAND")
|
||||
.help("A command to run with its package downloads screened")
|
||||
.num_args(1..)
|
||||
.trailing_var_arg(true)
|
||||
.allow_hyphen_values(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn serve() -> ! {
|
||||
@@ -82,7 +70,7 @@ fn serve() -> ! {
|
||||
}
|
||||
|
||||
fn run(invocation: Invocation) -> ! {
|
||||
let command = invocation.manager.command;
|
||||
let command = &invocation.program;
|
||||
let port = match aliasing::spawn(0, load()) {
|
||||
Ok(port) => port,
|
||||
Err(e) => fail("spawn", &format!("could not start the firewall: {e}")),
|
||||
@@ -105,12 +93,32 @@ fn print_setup(config: &FirewallConfig) {
|
||||
let base = config.local_url("");
|
||||
println!("Firewall listening on {base}\n");
|
||||
println!("Point your package managers at it:");
|
||||
println!(" npm npm_config_registry={base}/npm/");
|
||||
println!(" pip PIP_INDEX_URL={base}/pypi/simple/");
|
||||
println!(" npm {}", variable("npm_config_registry", &format!("{base}/npm/")));
|
||||
println!(" pip {}", variable("PIP_INDEX_URL", &format!("{base}/pypi/simple/")));
|
||||
println!(" cargo [source.crates-io] replace-with = \"firewall\"");
|
||||
println!(" [source.firewall] registry = \"sparse+{base}/cargo/index/\"\n");
|
||||
}
|
||||
|
||||
fn alias(program: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
// Set-Alias cannot carry arguments, so a function stands in.
|
||||
format!(
|
||||
"PowerShell: function npm {{ {program} npm @args }}\n \
|
||||
cmd: doskey npm={program} npm $*"
|
||||
)
|
||||
} else {
|
||||
format!("alias npm='{program} npm'")
|
||||
}
|
||||
}
|
||||
|
||||
fn variable(key: &str, value: &str) -> String {
|
||||
if cfg!(windows) {
|
||||
format!("$env:{key} = \"{value}\"")
|
||||
} else {
|
||||
format!("{key}={value}")
|
||||
}
|
||||
}
|
||||
|
||||
fn load() -> FirewallConfig {
|
||||
match FirewallConfig::load() {
|
||||
Ok(config) => config,
|
||||
@@ -127,7 +135,9 @@ fn fail(kind: &'static str, message: &str) -> ! {
|
||||
|
||||
fn program_name() -> String {
|
||||
let arg = std::env::args_os().next().unwrap_or_default();
|
||||
match Path::new(&arg).file_name() {
|
||||
// Without `.exe` on Windows, which is how it is typed.
|
||||
let path = Path::new(&arg);
|
||||
match if cfg!(windows) { path.file_stem() } else { path.file_name() } {
|
||||
Some(name) => name.to_string_lossy().into_owned(),
|
||||
None => "firewall".into(),
|
||||
}
|
||||
|
||||
+22
-3
@@ -1,10 +1,12 @@
|
||||
use rama::error::BoxError;
|
||||
use rama::tls::boring::core::x509::X509;
|
||||
use rcgen::{
|
||||
BasicConstraints, CertificateParams, DistinguishedName, DnType, GeneralSubtree, IsCa,
|
||||
KeyUsagePurpose, NameConstraints, SigningKey,
|
||||
BasicConstraints, CertificateParams, CertificateRevocationListParams, DistinguishedName,
|
||||
DnType, GeneralSubtree, IsCa, Issuer, KeyIdMethod, KeyUsagePurpose, NameConstraints,
|
||||
SerialNumber, SigningKey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
/// Held in the Secure Enclave on macOS, in memory elsewhere.
|
||||
pub type CaSigner = Arc<dyn SigningKey + Send + Sync>;
|
||||
@@ -42,6 +44,22 @@ impl CertificateAuthority {
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty, for verifiers that fail closed without one, such as schannel under cargo.
|
||||
/// Signed fresh for each fetch; a week's validity outlasts any verifier's cache.
|
||||
pub fn crl(hosts: &[&str], signer: &dyn SigningKey) -> Result<Vec<u8>, BoxError> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let list = CertificateRevocationListParams {
|
||||
this_update: now - Duration::hours(1),
|
||||
next_update: now + Duration::days(7),
|
||||
crl_number: SerialNumber::from(now.unix_timestamp() as u64),
|
||||
issuing_distribution_point: None,
|
||||
revoked_certs: Vec::new(),
|
||||
key_identifier_method: KeyIdMethod::Sha256,
|
||||
};
|
||||
let ca = params(hosts);
|
||||
Ok(list.signed_by(&Issuer::from_params(&ca, signer))?.der().to_vec())
|
||||
}
|
||||
|
||||
/// Also used by the issuer, whose issuer name must match the CA's subject.
|
||||
pub fn params(hosts: &[&str]) -> CertificateParams {
|
||||
let mut name = DistinguishedName::new();
|
||||
@@ -51,7 +69,8 @@ pub fn params(hosts: &[&str]) -> CertificateParams {
|
||||
let mut params = CertificateParams::default();
|
||||
params.distinguished_name = name;
|
||||
params.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
|
||||
params.key_usages = vec![KeyUsagePurpose::KeyCertSign];
|
||||
// rcgen signs a CRL only for a CA that may.
|
||||
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
|
||||
params.name_constraints = Some(NameConstraints {
|
||||
permitted_subtrees: hosts
|
||||
.iter()
|
||||
|
||||
+12
-3
@@ -4,7 +4,7 @@ use rama::tls::boring::core::pkey::PKey;
|
||||
use rama::tls::boring::core::x509::X509;
|
||||
use rama::tls::boring::proxy::cert_issuer::{BoringMitmCertIssuer, MitmIssuedCert};
|
||||
use rama::utils::collections::non_empty_vec;
|
||||
use rcgen::{CertificateParams, DistinguishedName, DnType, Issuer, KeyPair};
|
||||
use rcgen::{CertificateParams, CrlDistributionPoint, DistinguishedName, DnType, Issuer, KeyPair};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Leaves name only screened hosts; any other name falls outside the CA's constraint.
|
||||
@@ -14,12 +14,18 @@ pub struct ConstrainedCertIssuer {
|
||||
ca_params: Arc<CertificateParams>,
|
||||
signer: CaSigner,
|
||||
hosts: Vec<&'static str>,
|
||||
crl: Option<String>,
|
||||
}
|
||||
|
||||
impl ConstrainedCertIssuer {
|
||||
pub(super) fn new(ca_crt: X509, signer: CaSigner, hosts: Vec<&'static str>) -> Self {
|
||||
pub(super) fn new(
|
||||
ca_crt: X509,
|
||||
signer: CaSigner,
|
||||
hosts: Vec<&'static str>,
|
||||
crl: Option<String>,
|
||||
) -> Self {
|
||||
let ca_params = Arc::new(params(&hosts));
|
||||
Self { ca_crt, ca_params, signer, hosts }
|
||||
Self { ca_crt, ca_params, signer, hosts, crl }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +48,9 @@ impl BoringMitmCertIssuer for ConstrainedCertIssuer {
|
||||
subject.push(DnType::CommonName, common_name);
|
||||
let mut leaf = CertificateParams::new(names)?;
|
||||
leaf.distinguished_name = subject;
|
||||
if let Some(crl) = &self.crl {
|
||||
leaf.crl_distribution_points = vec![CrlDistributionPoint { uris: vec![crl.clone()] }];
|
||||
}
|
||||
|
||||
// The TLS server needs the leaf key in memory, so it can't be the platform key.
|
||||
let leaf_key = KeyPair::generate()?;
|
||||
|
||||
@@ -3,7 +3,7 @@ mod issuer;
|
||||
mod route;
|
||||
mod screen;
|
||||
|
||||
pub use ca::{CaSigner, CertificateAuthority};
|
||||
pub use ca::{crl, CaSigner, CertificateAuthority};
|
||||
|
||||
use crate::Firewall;
|
||||
use issuer::ConstrainedCertIssuer;
|
||||
@@ -25,14 +25,16 @@ type RelayService = Arc<HttpMitmRelay<FirewallLayer>>;
|
||||
type InterceptService =
|
||||
TlsMitmRelayService<CachedBoringMitmCertIssuer<ConstrainedCertIssuer>, RelayService>;
|
||||
|
||||
/// `crl` is where leaves say an empty CRL is served, for verifiers that insist on one.
|
||||
pub fn mitm_service(
|
||||
firewall: Arc<Firewall>,
|
||||
ca: &CertificateAuthority,
|
||||
signer: CaSigner,
|
||||
crl: Option<String>,
|
||||
exec: Executor,
|
||||
) -> MitmService {
|
||||
let hosts = firewall.hosts();
|
||||
let issuer = ConstrainedCertIssuer::new(ca.crt.clone(), signer, hosts.clone());
|
||||
let issuer = ConstrainedCertIssuer::new(ca.crt.clone(), signer, hosts.clone(), crl);
|
||||
|
||||
let relay = Arc::new(HttpMitmRelay::new(exec).with_http_middleware(FirewallLayer { firewall }));
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ fn handler(exec: Executor) -> Result<Handler, BoxError> {
|
||||
let signer: mitm::CaSigner = Arc::new(key);
|
||||
let ca = install::certificate_authority(&hosts, &*signer);
|
||||
let ca = Arc::new(ca.inspect_err(|_| telemetry::error("ca"))?);
|
||||
let service = mitm::mitm_service(firewall, &ca, signer, exec);
|
||||
let service = mitm::mitm_service(firewall, &ca, signer, None, exec);
|
||||
Ok(Handler { ca, hosts, service })
|
||||
}
|
||||
|
||||
|
||||
+21
-1
@@ -1,10 +1,19 @@
|
||||
use firewall::aliasing::managers::*;
|
||||
use firewall::aliasing::Invocation;
|
||||
use firewall::Firewall;
|
||||
|
||||
#[test]
|
||||
fn arguments_after_a_double_dash_reach_the_program() {
|
||||
let typed = ["run", "--", "--release"].map(String::from).to_vec();
|
||||
let args = Invocation::new("cargo".into(), typed).arguments("http://127.0.0.1:9000");
|
||||
assert_eq!(args.first().map(String::as_str), Some("run"));
|
||||
assert_eq!(args[args.len() - 2..], ["--", "--release"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recipes_expand_into_the_environment() {
|
||||
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
|
||||
let npm = variables("http://127.0.0.1:9000", &NPM);
|
||||
let npm = variables("http://127.0.0.1:9000");
|
||||
assert!(npm.contains(&var("npm_config_proxy", "http://127.0.0.1:9000")));
|
||||
assert!(npm.contains(&var("npm_config_registry", "http://registry.npmjs.org/")));
|
||||
let gem = arguments("http://127.0.0.1:9000", &GEM, &["install".to_string()]);
|
||||
@@ -12,6 +21,17 @@ fn recipes_expand_into_the_environment() {
|
||||
assert!(arguments("http://127.0.0.1:9000", &GEM, &["sources".to_string()]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_two_managers_set_a_variable_differently() {
|
||||
let mut seen = std::collections::HashMap::new();
|
||||
for manager in MANAGERS {
|
||||
for (key, value) in manager.env {
|
||||
let first = *seen.entry(key).or_insert(value);
|
||||
assert_eq!(first, value, "{} sets {key} differently", manager.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn every_overridden_registry_is_known() {
|
||||
let hosts = Firewall::default().hosts();
|
||||
|
||||
@@ -43,3 +43,10 @@ fn stored_ca_is_rejected_when_the_signing_key_changed() {
|
||||
|
||||
assert!(mitm::CertificateAuthority::from_blob(&blob, &hosts, &signer()).is_none());
|
||||
}
|
||||
|
||||
/// rcgen signs a CRL only for a CA with cRLSign, and schannel wants one for every leaf.
|
||||
#[test]
|
||||
fn ca_signs_an_empty_crl() {
|
||||
let crl = mitm::crl(&["registry.npmjs.org", "pypi.org"], &signer()).expect("sign a crl");
|
||||
assert_eq!(crl[0], 0x30, "a DER SEQUENCE");
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub fn all(port: u16) -> Vec<(&'static str, Outcome)> {
|
||||
fn run(probe: &Probe, port: u16) -> Outcome {
|
||||
let args = probe.args.iter().map(|arg| arg.to_string()).collect();
|
||||
let dir = probe.setup.and_then(|prepare| prepare());
|
||||
match aliasing::run(port, &Invocation::new(probe.manager, args), dir.as_deref()) {
|
||||
match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref()) {
|
||||
Ok(0) => Outcome::Ok,
|
||||
Ok(_) => Outcome::Failed,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,
|
||||
|
||||
Reference in new issue
Block a user