Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 95 additions & 6 deletions members/nullnet-proxy/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,18 +238,62 @@ impl ProxyHttp for NullnetProxy {

Ok(Box::new(HttpPeer::new(upstream, false, String::new())))
}

async fn upstream_request_filter(
&self,
session: &mut Session,
upstream_request: &mut RequestHeader,
_ctx: &mut Self::CTX,
) -> Result<()> {
// TLS terminates at this proxy, so it is the sole source of truth for
// client IP, scheme, and requested host; any X-Forwarded-* the client
// itself sent is untrusted and must be overwritten, not appended to.
set_forwarded_headers(
upstream_request,
ingress_client_ip(session),
self.tls,
ingress_raw_host(session),
)
}
}

/// The requested service name (Host header, or HTTP/2 `:authority` via the URI),
/// with any port stripped. `None` if neither carries a host.
fn ingress_host(session: &Session) -> Option<String> {
/// Overwrite the X-Forwarded-{For,Proto,Host} headers on the upstream request
/// with the ingress connection's real client IP, scheme, and requested host.
fn set_forwarded_headers(
upstream_request: &mut RequestHeader,
client_ip: Option<String>,
tls: bool,
host: Option<String>,
) -> Result<()> {
if let Some(client_ip) = client_ip {
upstream_request.insert_header("X-Forwarded-For", client_ip)?;
}

let proto = if tls { "https" } else { "http" };
upstream_request.insert_header("X-Forwarded-Proto", proto)?;

if let Some(host) = host {
upstream_request.insert_header("X-Forwarded-Host", host)?;
}

Ok(())
}

/// The original Host header (or HTTP/2 `:authority` via the URI) verbatim, port
/// included if present. `None` if neither carries a host.
fn ingress_raw_host(session: &Session) -> Option<String> {
let req = session.req_header();
let raw = req
.headers
req.headers
.get("host")
.and_then(|v| v.to_str().ok())
.map(str::to_string)
.or_else(|| req.uri.host().map(str::to_string))?;
.or_else(|| req.uri.host().map(str::to_string))
}

/// The requested service name (Host header, or HTTP/2 `:authority` via the URI),
/// with any port stripped. `None` if neither carries a host.
fn ingress_host(session: &Session) -> Option<String> {
let raw = ingress_raw_host(session)?;
Some(raw.split(':').next().unwrap_or(&raw).to_string())
}

Expand Down Expand Up @@ -416,6 +460,51 @@ mod tests {
r.remove_header("host");
assert_eq!(https_redirect_url(&r, 443), "https:///");
}

#[test]
fn forwarded_headers_are_set_from_ingress_connection() {
let mut r = req("color.com", "/");
set_forwarded_headers(
&mut r,
Some("203.0.113.7".to_string()),
true,
Some("color.com".to_string()),
)
.unwrap();
assert_eq!(r.headers.get("x-forwarded-for").unwrap(), "203.0.113.7");
assert_eq!(r.headers.get("x-forwarded-proto").unwrap(), "https");
assert_eq!(r.headers.get("x-forwarded-host").unwrap(), "color.com");
}

#[test]
fn forwarded_proto_is_http_when_not_tls() {
let mut r = req("color.com", "/");
set_forwarded_headers(&mut r, None, false, None).unwrap();
assert_eq!(r.headers.get("x-forwarded-proto").unwrap(), "http");
}

#[test]
fn forwarded_headers_overwrite_client_supplied_values() {
let mut r = req("color.com", "/");
r.insert_header("x-forwarded-for", "1.2.3.4").unwrap();
r.insert_header("x-forwarded-proto", "http").unwrap();
set_forwarded_headers(
&mut r,
Some("203.0.113.7".to_string()),
true,
Some("color.com".to_string()),
)
.unwrap();
assert_eq!(r.headers.get("x-forwarded-for").unwrap(), "203.0.113.7");
assert_eq!(r.headers.get("x-forwarded-proto").unwrap(), "https");
}

#[test]
fn forwarded_host_absent_when_ingress_host_unknown() {
let mut r = req("color.com", "/");
set_forwarded_headers(&mut r, None, false, None).unwrap();
assert!(r.headers.get("x-forwarded-host").is_none());
}
}

// fn redirect_stdout_stderr_to_file()
Expand Down