add TLS, service persistence, blocking panel, query types
- Local TLS: auto-generated CA + per-service certs (explicit SANs, not wildcards — browsers reject *.numa under single-label TLDs). HTTPS proxy on :443 via rustls/tokio-rustls. `numa install` trusts CA in macOS Keychain / Linux ca-certificates. - Service persistence: user-added services saved to ~/.config/numa/services.json, survive restarts. - Blocking panel: renamed "Check Domain" to "Blocking" with sources display, allowlist management UI, unpause button. - Query types: recognize SOA, PTR, TXT, SRV, HTTPS (type 65) instead of logging as UNKNOWN. - Blocklist gzip: reqwest now decompresses gzip responses from CDNs. - Unified config_dir() in lib.rs for consistent path resolution under sudo and launchd. TLS certs use /usr/local/var/numa/ (writable as root daemon). - Dashboard UX: panel subtitles differentiating overrides vs services, better placeholders, proxy route display, 600px query log height. - Deploy: make deploy handles build+copy+codesign+restart cycle. - Demo: scripts/record-demo.sh for recording hero GIF with CDP. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
76
src/proxy.rs
76
src/proxy.rs
@@ -11,7 +11,9 @@ use hyper::StatusCode;
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use log::{debug, error, info, warn};
|
||||
use rustls::ServerConfig;
|
||||
use tokio::io::copy_bidirectional;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use crate::ctx::ServerCtx;
|
||||
|
||||
@@ -21,10 +23,9 @@ type HttpClient = Client<hyper_util::client::legacy::connect::HttpConnector, Bod
|
||||
struct ProxyState {
|
||||
ctx: Arc<ServerCtx>,
|
||||
client: HttpClient,
|
||||
tld_suffix: String, // pre-computed ".{tld}"
|
||||
}
|
||||
|
||||
pub async fn start_proxy(ctx: Arc<ServerCtx>, port: u16, tld: &str) {
|
||||
pub async fn start_proxy(ctx: Arc<ServerCtx>, port: u16) {
|
||||
let addr: SocketAddr = ([0, 0, 0, 0], port).into();
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
@@ -45,7 +46,6 @@ pub async fn start_proxy(ctx: Arc<ServerCtx>, port: u16, tld: &str) {
|
||||
let state = ProxyState {
|
||||
ctx,
|
||||
client,
|
||||
tld_suffix: format!(".{}", tld),
|
||||
};
|
||||
|
||||
let app = Router::new().fallback(any(proxy_handler)).with_state(state);
|
||||
@@ -53,6 +53,68 @@ pub async fn start_proxy(ctx: Arc<ServerCtx>, port: u16, tld: &str) {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
pub async fn start_proxy_tls(ctx: Arc<ServerCtx>, port: u16, tls_config: Arc<ServerConfig>) {
|
||||
let addr: SocketAddr = ([0, 0, 0, 0], port).into();
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"proxy: could not bind TLS port {} ({}) — HTTPS proxy disabled",
|
||||
port, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
info!("HTTPS proxy listening on {}", addr);
|
||||
|
||||
let acceptor = TlsAcceptor::from(tls_config);
|
||||
let client: HttpClient = Client::builder(TokioExecutor::new())
|
||||
.http1_preserve_header_case(true)
|
||||
.build_http();
|
||||
|
||||
let state = ProxyState {
|
||||
ctx,
|
||||
client,
|
||||
};
|
||||
|
||||
let app = Router::new().fallback(any(proxy_handler)).with_state(state);
|
||||
|
||||
loop {
|
||||
let (tcp_stream, remote_addr) = match listener.accept().await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
error!("TLS accept error: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let acceptor = acceptor.clone();
|
||||
let app = app.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let tls_stream = match acceptor.accept(tcp_stream).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
debug!("TLS handshake failed from {}: {}", remote_addr, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let io = hyper_util::rt::TokioIo::new(tls_stream);
|
||||
let svc = hyper_util::service::TowerToHyperService::new(app.into_service());
|
||||
|
||||
if let Err(e) = hyper::server::conn::http1::Builder::new()
|
||||
.preserve_header_case(true)
|
||||
.serve_connection(io, svc)
|
||||
.with_upgrades()
|
||||
.await
|
||||
{
|
||||
debug!("TLS connection error from {}: {}", remote_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_host(req: &Request) -> Option<String> {
|
||||
req.headers()
|
||||
.get(hyper::header::HOST)
|
||||
@@ -68,12 +130,12 @@ async fn proxy_handler(State(state): State<ProxyState>, req: Request) -> axum::r
|
||||
}
|
||||
};
|
||||
|
||||
let service_name = match hostname.strip_suffix(state.tld_suffix.as_str()) {
|
||||
let service_name = match hostname.strip_suffix(state.ctx.proxy_tld_suffix.as_str()) {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("not a {} domain: {}", state.tld_suffix, hostname),
|
||||
format!("not a {} domain: {}", state.ctx.proxy_tld_suffix, hostname),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -86,7 +148,7 @@ async fn proxy_handler(State(state): State<ProxyState>, req: Request) -> axum::r
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("unknown service: {}{}", service_name, state.tld_suffix),
|
||||
format!("unknown service: {}{}", service_name, state.ctx.proxy_tld_suffix),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -98,7 +160,7 @@ async fn proxy_handler(State(state): State<ProxyState>, req: Request) -> axum::r
|
||||
.path_and_query()
|
||||
.map(|pq| pq.as_str())
|
||||
.unwrap_or("/");
|
||||
let target_uri: hyper::Uri = format!("http://127.0.0.1:{}{}", target_port, path_and_query)
|
||||
let target_uri: hyper::Uri = format!("http://localhost:{}{}", target_port, path_and_query)
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user