# nono-proxy
Network filtering proxy for the [nono](https://crates.io/crates/nono) sandbox.
## Overview
`nono-proxy` provides host-level network filtering and credential injection for sandboxed processes. It runs **unsandboxed** in the supervisor process while the child is restricted to connecting only to the proxy's localhost port via `NetworkMode::ProxyOnly`.
## Proxy Modes
| CONNECT tunnel | `connect` | Host-filtered HTTPS tunnelling. Validates the target host against an allowlist and cloud metadata deny list, then establishes a raw TCP tunnel. TLS is end-to-end. |
| Reverse proxy | `reverse` | Credential injection for API calls. Requests to `http://127.0.0.1:<port>/<service>/...` are forwarded upstream with the real API key injected as an HTTP header. |
| External proxy | `external` | Enterprise proxy passthrough. CONNECT requests are chained through a corporate proxy with cloud metadata endpoints still denied. |
## Security Properties
- **Cloud metadata deny list is hardcoded** -- Cloud metadata hostnames (169.254.169.254, metadata.google.internal, metadata.azure.internal) are always blocked regardless of allowlist configuration. Private network addresses (RFC1918) are allowed to support enterprise environments.
- **DNS rebinding protection** -- The proxy resolves DNS, checks all resolved IPs against the link-local range (169.254.0.0/16, fe80::/10), and connects to resolved addresses (not re-resolved hostnames). This prevents DNS rebinding attacks targeting cloud metadata.
- **Session token authentication** -- Each session generates a 256-bit random token. CONNECT requests use `Proxy-Authorization` (Basic or Bearer); reverse proxy requests use `X-Nono-Token`.
- **Credential isolation** -- API keys are loaded from the OS keyring, stored in `Zeroizing<String>`, injected at the HTTP header level, and never exposed to the sandboxed process.
- **Constant-time token comparison** -- Prevents timing side-channel attacks on session token validation.
## Usage
```rust
use nono_proxy::{ProxyConfig, start, ProxyHandle};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ProxyConfig {
allowed_hosts: vec![
"api.openai.com".into(),
"api.anthropic.com".into(),
],
..Default::default()
};
let handle: ProxyHandle = start(config).await?;
// Set these in the child process environment
let env_vars = handle.env_vars();
// HTTP_PROXY, HTTPS_PROXY, NONO_PROXY_TOKEN, etc.
// Shutdown when done
handle.shutdown();
Ok(())
}
```
## Module Structure
| `server` | TCP listener, connection dispatch, lifecycle |
| `filter` | Async host filtering with DNS resolution |
| `connect` | CONNECT tunnel handler |
| `reverse` | Reverse proxy with credential injection |
| `external` | External proxy passthrough |
| `credential` | Keyring-backed credential store |
| `token` | Session token generation and validation |
| `config` | Configuration types |
| `audit` | Connection audit logging |
| `error` | Error types |
## License
Apache-2.0