Compare commits

..
Author SHA1 Message Date
Ian Graham MartinezandClaude Opus 5.5 098dea7cc0 Trust the PyPI hosts for pipenv's pip
pipenv installs through pip, which skips the plaintext index on an
untrusted host, so `firewall pipenv install six` fetched nothing and
failed with "No matching distribution found". Set PIP_TRUSTED_HOST as
the pip recipe already does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 15:30:43 -04:00
9 changed files with 78 additions and 215 deletions

No files matched your search

+7 -108
View File
@@ -45,56 +45,6 @@ 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"
@@ -260,6 +210,12 @@ 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"
@@ -357,33 +313,6 @@ 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"
@@ -393,12 +322,6 @@ 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"
@@ -1177,12 +1100,6 @@ 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"
@@ -1645,12 +1562,6 @@ 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"
@@ -3093,7 +3004,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
name = "strix-firewall"
version = "0.1.0"
dependencies = [
"clap",
"bpaf",
"core-foundation",
"directories",
"percent-encoding",
@@ -3113,12 +3024,6 @@ 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"
@@ -3528,12 +3433,6 @@ 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
View File
@@ -53,7 +53,7 @@ name = "managers"
path = "tests/managers.rs"
[dependencies]
clap = "4.6.6"
bpaf = "0.9.28"
directories = "6.0.0"
percent-encoding = "2.3.2"
rama = { version = "0.4.0", features = ["boring", "http-full", "ring", "rustls"] }
+1 -24
View File
@@ -1,20 +1,15 @@
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;
@@ -36,26 +31,8 @@ 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(),
tunnel,
)
(RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop())
.into_layer(service_fn(move |req: Request| {
let firewall = Arc::clone(&firewall);
async move { handle(req, &firewall).await }
+2 -4
View File
@@ -41,10 +41,8 @@ pub struct Manager {
pub static MANAGERS: &[&Manager] =
&[&NPM, &PIP, &PIP3, &CARGO, &GO, &GEM, &UV, &PDM, &PIPENV, &PNPM, &BUN];
/// 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 variables(proxy: &str, manager: &Manager) -> Vec<(String, String)> {
manager.env.iter().map(|(key, value)| (key.to_string(), expand(value, proxy))).collect()
}
pub fn arguments(proxy: &str, manager: &Manager, typed: &[String]) -> Vec<String> {
+2
View File
@@ -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: &[],
};
+12 -19
View File
@@ -9,37 +9,30 @@ use std::path::{Path, PathBuf};
use std::process::Command;
pub struct Invocation {
pub program: String,
pub manager: &'static Manager,
pub args: Vec<String>,
}
impl Invocation {
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 new(manager: &'static Manager, args: Vec<String>) -> Self {
Invocation { manager, args }
}
}
pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::Result<i32> {
let proxy = crate::local_url(port, "");
let mut command = Command::new(resolve(&invocation.program)?);
command.args(invocation.arguments(&proxy));
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);
if let Some(dir) = dir {
command.current_dir(dir);
}
for (key, value) in managers::variables(&proxy) {
for (key, value) in managers::variables(&proxy, invocation.manager) {
command.env(key, value);
}
Ok(command.status()?.code().unwrap_or(1))
+51 -37
View File
@@ -1,50 +1,64 @@
use clap::{Arg, Command};
use firewall::aliasing::{self, Invocation, MANAGERS};
use bpaf::{any, choice, pure, OptionParser, Parser};
use firewall::aliasing::{self, Invocation, Manager, MANAGERS};
use firewall::{config, proxy, telemetry, FirewallConfig};
use std::io::ErrorKind;
use std::path::Path;
use std::process::exit;
use tracing::{error, info};
fn main() {
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()))
enum SubCommand {
Serve,
Run(Invocation),
}
fn options() -> Command {
fn main() {
match options().run() {
SubCommand::Serve => {
firewall::init_logging("info");
serve()
}
SubCommand::Run(invocation) => {
firewall::init_logging("warn");
run(invocation)
}
}
}
fn options() -> OptionParser<SubCommand> {
let program = program_name();
let managers: Vec<_> = MANAGERS.iter().map(|manager| manager.command).collect();
let footer = format!(
"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 \
alias npm='{program} npm'\n\n\
Supported package managers: {}",
managers.join(", ")
"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'"
);
Command::new("firewall")
.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),
)
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)
}
fn serve() -> ! {
@@ -68,7 +82,7 @@ fn serve() -> ! {
}
fn run(invocation: Invocation) -> ! {
let command = &invocation.program;
let command = invocation.manager.command;
let port = match aliasing::spawn(0, load()) {
Ok(port) => port,
Err(e) => fail("spawn", &format!("could not start the firewall: {e}")),
+1 -21
View File
@@ -1,19 +1,10 @@
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");
let npm = variables("http://127.0.0.1:9000", &NPM);
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()]);
@@ -21,17 +12,6 @@ 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();
+1 -1
View File
@@ -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.command.into(), args), dir.as_deref()) {
match aliasing::run(port, &Invocation::new(probe.manager, args), dir.as_deref()) {
Ok(0) => Outcome::Ok,
Ok(_) => Outcome::Failed,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,