proxy: forward v2 TLS TLVs (PP2_TYPE_SSL/CERTFP) so plaintext clients behind a TLS-terminating proxy show secure+certfp; restore ws_trust_proxy to the config example
This commit is contained in:
parent
4f6c0ded48
commit
6ffd6a57bc
3 changed files with 128 additions and 23 deletions
|
|
@ -149,7 +149,9 @@ amu_target = both
|
||||||
# ws_origin = https://x.example # (repeatable) allowed Origin globs; empty = any
|
# ws_origin = https://x.example # (repeatable) allowed Origin globs; empty = any
|
||||||
# ws_defaultmode = text # frame mode with no subprotocol: text|binary|reject
|
# ws_defaultmode = text # frame mode with no subprotocol: text|binary|reject
|
||||||
# ws_proxyranges = 127.0.0.1 # (repeatable) glob/CIDR of proxies whose
|
# ws_proxyranges = 127.0.0.1 # (repeatable) glob/CIDR of proxies whose
|
||||||
# # X-Real-IP / X-Forwarded-For we trust
|
# # X-Real-IP / X-Forwarded-For we trust (scoped)
|
||||||
|
# ws_trust_proxy = no # trust those headers from ANY peer (simpler but
|
||||||
|
# # allows IP spoofing; prefer ws_proxyranges)
|
||||||
# ws_allowmissingorigin = yes # allow clients that send no Origin header
|
# ws_allowmissingorigin = yes # allow clients that send no Origin header
|
||||||
# ws_nativeping = yes # liveness via WebSocket pings (no = IRC PING)
|
# ws_nativeping = yes # liveness via WebSocket pings (no = IRC PING)
|
||||||
# ws_handshake_timeout = 10 # seconds to complete the HTTP Upgrade
|
# ws_handshake_timeout = 10 # seconds to complete the HTTP Upgrade
|
||||||
|
|
|
||||||
111
src/proxy.rs
111
src/proxy.rs
|
|
@ -19,8 +19,14 @@ const V1_MAX: usize = 107;
|
||||||
|
|
||||||
/// The result of trying to parse a PROXY header from a byte prefix.
|
/// The result of trying to parse a PROXY header from a byte prefix.
|
||||||
pub enum Parsed {
|
pub enum Parsed {
|
||||||
/// A full header giving the real client (source) address.
|
/// A full header giving the real client (source) address, plus any TLS metadata
|
||||||
Proxy(SocketAddr),
|
/// a v2 header forwarded (a TLS-terminating proxy sets `secure` and, if it
|
||||||
|
/// forwards one, the client cert `certfp`).
|
||||||
|
Proxy {
|
||||||
|
addr: SocketAddr,
|
||||||
|
secure: bool,
|
||||||
|
certfp: Option<String>,
|
||||||
|
},
|
||||||
/// A full header with no address to apply (LOCAL / unsupported family).
|
/// A full header with no address to apply (LOCAL / unsupported family).
|
||||||
Local,
|
Local,
|
||||||
/// Not enough bytes yet — read more and retry.
|
/// Not enough bytes yet — read more and retry.
|
||||||
|
|
@ -91,7 +97,14 @@ fn parse_v1(buf: &[u8]) -> (Parsed, usize) {
|
||||||
return (Parsed::Invalid, consumed);
|
return (Parsed::Invalid, consumed);
|
||||||
}
|
}
|
||||||
match (p[2].parse::<IpAddr>(), p[4].parse::<u16>()) {
|
match (p[2].parse::<IpAddr>(), p[4].parse::<u16>()) {
|
||||||
(Ok(ip), Ok(port)) => (Parsed::Proxy(SocketAddr::new(ip, port)), consumed),
|
(Ok(ip), Ok(port)) => (
|
||||||
|
Parsed::Proxy {
|
||||||
|
addr: SocketAddr::new(ip, port),
|
||||||
|
secure: false,
|
||||||
|
certfp: None,
|
||||||
|
},
|
||||||
|
consumed,
|
||||||
|
),
|
||||||
_ => (Parsed::Invalid, consumed),
|
_ => (Parsed::Invalid, consumed),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,23 +135,68 @@ fn parse_v2(buf: &[u8]) -> (Parsed, usize) {
|
||||||
return (Parsed::Invalid, total);
|
return (Parsed::Invalid, total);
|
||||||
}
|
}
|
||||||
let a = &buf[16..total];
|
let a = &buf[16..total];
|
||||||
match family {
|
let (addr, fixed) = match family {
|
||||||
1 if len >= 12 => {
|
1 if len >= 12 => {
|
||||||
let src = Ipv4Addr::new(a[0], a[1], a[2], a[3]);
|
let src = Ipv4Addr::new(a[0], a[1], a[2], a[3]);
|
||||||
let sport = u16::from_be_bytes([a[8], a[9]]);
|
let sport = u16::from_be_bytes([a[8], a[9]]);
|
||||||
(Parsed::Proxy(SocketAddr::new(IpAddr::V4(src), sport)), total)
|
(SocketAddr::new(IpAddr::V4(src), sport), 12)
|
||||||
}
|
}
|
||||||
2 if len >= 36 => {
|
2 if len >= 36 => {
|
||||||
let mut o = [0u8; 16];
|
let mut o = [0u8; 16];
|
||||||
o.copy_from_slice(&a[0..16]);
|
o.copy_from_slice(&a[0..16]);
|
||||||
let sport = u16::from_be_bytes([a[32], a[33]]);
|
let sport = u16::from_be_bytes([a[32], a[33]]);
|
||||||
(
|
(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(o)), sport), 36)
|
||||||
Parsed::Proxy(SocketAddr::new(IpAddr::V6(Ipv6Addr::from(o)), sport)),
|
|
||||||
total,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
_ => (Parsed::Local, total), // AF_UNIX / unspecified: keep peer addr
|
_ => return (Parsed::Local, total), // AF_UNIX / unspecified: keep peer addr
|
||||||
|
};
|
||||||
|
// any bytes after the fixed address are TLVs: a TLS-terminating proxy may
|
||||||
|
// forward the client's TLS status (PP2_TYPE_SSL) and cert fingerprint (CERTFP)
|
||||||
|
let (secure, certfp) = parse_v2_tlvs(&a[fixed..]);
|
||||||
|
(
|
||||||
|
Parsed::Proxy {
|
||||||
|
addr,
|
||||||
|
secure,
|
||||||
|
certfp,
|
||||||
|
},
|
||||||
|
total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROXY v2 TLV types we care about.
|
||||||
|
const PP2_TYPE_SSL: u8 = 0x20;
|
||||||
|
const PP2_TYPE_CERTFP: u8 = 0xE0;
|
||||||
|
const PP2_CLIENT_SSL: u8 = 0x01;
|
||||||
|
|
||||||
|
/// Walk the v2 TLV block: `type(1) len(2, big-endian) value(len)`. Returns whether
|
||||||
|
/// the client was on TLS and its forwarded cert fingerprint, if any.
|
||||||
|
fn parse_v2_tlvs(mut tlv: &[u8]) -> (bool, Option<String>) {
|
||||||
|
let mut secure = false;
|
||||||
|
let mut certfp = None;
|
||||||
|
while tlv.len() >= 3 {
|
||||||
|
let ttype = tlv[0];
|
||||||
|
let tlen = u16::from_be_bytes([tlv[1], tlv[2]]) as usize;
|
||||||
|
if tlv.len() < 3 + tlen {
|
||||||
|
break; // truncated TLV
|
||||||
|
}
|
||||||
|
let val = &tlv[3..3 + tlen];
|
||||||
|
match ttype {
|
||||||
|
PP2_TYPE_SSL => {
|
||||||
|
if !val.is_empty() && val[0] & PP2_CLIENT_SSL != 0 {
|
||||||
|
secure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PP2_TYPE_CERTFP => {
|
||||||
|
if let Ok(s) = std::str::from_utf8(val) {
|
||||||
|
if !s.is_empty() && s.len() <= 128 && s.bytes().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
certfp = Some(s.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
tlv = &tlv[3 + tlen..];
|
||||||
}
|
}
|
||||||
|
(secure, certfp)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -147,7 +205,7 @@ mod tests {
|
||||||
|
|
||||||
fn src(p: &Parsed) -> Option<SocketAddr> {
|
fn src(p: &Parsed) -> Option<SocketAddr> {
|
||||||
match p {
|
match p {
|
||||||
Parsed::Proxy(a) => Some(*a),
|
Parsed::Proxy { addr, .. } => Some(*addr),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,6 +250,37 @@ mod tests {
|
||||||
assert_eq!(n, 28);
|
assert_eq!(n, 28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn v2_tls_tlvs() {
|
||||||
|
// a TLS-terminating proxy forwards PP2_TYPE_SSL (client-on-TLS) + CERTFP
|
||||||
|
let mut h = V2_SIG.to_vec();
|
||||||
|
h.push(0x21); // v2, PROXY
|
||||||
|
h.push(0x11); // AF_INET, STREAM
|
||||||
|
h.extend_from_slice(&31u16.to_be_bytes()); // 12 addr + 8 SSL TLV + 11 CERTFP TLV
|
||||||
|
h.extend_from_slice(&[198, 51, 100, 10]); // src
|
||||||
|
h.extend_from_slice(&[10, 0, 0, 1]); // dst
|
||||||
|
h.extend_from_slice(&5000u16.to_be_bytes());
|
||||||
|
h.extend_from_slice(&443u16.to_be_bytes());
|
||||||
|
h.push(0x20); // PP2_TYPE_SSL
|
||||||
|
h.extend_from_slice(&5u16.to_be_bytes());
|
||||||
|
h.extend_from_slice(&[0x01, 0, 0, 0, 0]); // client=PP2_CLIENT_SSL, verify=0
|
||||||
|
h.push(0xE0); // PP2_TYPE_CERTFP
|
||||||
|
h.extend_from_slice(&8u16.to_be_bytes());
|
||||||
|
h.extend_from_slice(b"abcd1234");
|
||||||
|
match parse(&h).0 {
|
||||||
|
Parsed::Proxy {
|
||||||
|
addr,
|
||||||
|
secure,
|
||||||
|
certfp,
|
||||||
|
} => {
|
||||||
|
assert_eq!(addr.to_string(), "198.51.100.10:5000");
|
||||||
|
assert!(secure);
|
||||||
|
assert_eq!(certfp.as_deref(), Some("abcd1234"));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Proxy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v2_partial_and_local() {
|
fn v2_partial_and_local() {
|
||||||
assert!(matches!(parse(&V2_SIG[..8]), (Parsed::Need, _)));
|
assert!(matches!(parse(&V2_SIG[..8]), (Parsed::Need, _)));
|
||||||
|
|
|
||||||
|
|
@ -369,7 +369,7 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
let mut chunk = [0u8; 8192];
|
let mut chunk = [0u8; 8192];
|
||||||
let mut lines: Vec<(Uid, String)> = Vec::new();
|
let mut lines: Vec<(Uid, String)> = Vec::new();
|
||||||
// a deferred Connect (PROXY conn) to emit, before any lines from the same read
|
// a deferred Connect (PROXY conn) to emit, before any lines from the same read
|
||||||
let mut connect: Option<(Uid, SocketAddr, u16, OutSink)> = None;
|
let mut connect: Option<(Uid, SocketAddr, u16, bool, Option<String>, OutSink)> = None;
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
if let Some(c) = conns.get_mut(&t) {
|
if let Some(c) = conns.get_mut(&t) {
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -381,6 +381,10 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
c.rbuf.extend_from_slice(&chunk[..n]);
|
c.rbuf.extend_from_slice(&chunk[..n]);
|
||||||
if c.proxy_pending {
|
if c.proxy_pending {
|
||||||
|
// a v2 header from a TLS-terminating proxy can forward the
|
||||||
|
// client's TLS status + cert fingerprint (see modules::proxy)
|
||||||
|
let mut psecure = false;
|
||||||
|
let mut pcertfp: Option<String> = None;
|
||||||
match crate::proxy::parse(&c.rbuf) {
|
match crate::proxy::parse(&c.rbuf) {
|
||||||
(crate::proxy::Parsed::Need, _) => {
|
(crate::proxy::Parsed::Need, _) => {
|
||||||
if c.rbuf.len() > PROXY_MAX {
|
if c.rbuf.len() > PROXY_MAX {
|
||||||
|
|
@ -393,10 +397,19 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
close = true;
|
close = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
(crate::proxy::Parsed::Proxy(real), used) => {
|
(
|
||||||
c.addr = real; // rewrite to the real client address
|
crate::proxy::Parsed::Proxy {
|
||||||
|
addr,
|
||||||
|
secure,
|
||||||
|
certfp,
|
||||||
|
},
|
||||||
|
used,
|
||||||
|
) => {
|
||||||
|
c.addr = addr; // rewrite to the real client address
|
||||||
c.rbuf.drain(..used);
|
c.rbuf.drain(..used);
|
||||||
c.proxy_pending = false;
|
c.proxy_pending = false;
|
||||||
|
psecure = secure;
|
||||||
|
pcertfp = certfp;
|
||||||
}
|
}
|
||||||
(crate::proxy::Parsed::Local, used) => {
|
(crate::proxy::Parsed::Local, used) => {
|
||||||
c.rbuf.drain(..used); // keep the peer addr
|
c.rbuf.drain(..used); // keep the peer addr
|
||||||
|
|
@ -404,10 +417,9 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !c.proxy_pending {
|
if !c.proxy_pending {
|
||||||
connect = c
|
connect = c.pending_out.take().map(|out| {
|
||||||
.pending_out
|
(c.uid, c.addr, c.local_port, psecure, pcertfp, out)
|
||||||
.take()
|
});
|
||||||
.map(|out| (c.uid, c.addr, c.local_port, out));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !c.proxy_pending {
|
if !c.proxy_pending {
|
||||||
|
|
@ -433,15 +445,15 @@ fn read_conn(poll: &mut Poll, conns: &mut HashMap<usize, Conn>, t: usize, core:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((uid, addr, local_port, out)) = connect {
|
if let Some((uid, addr, local_port, secure, certfp, out)) = connect {
|
||||||
if core
|
if core
|
||||||
.send(Event::Connect {
|
.send(Event::Connect {
|
||||||
uid,
|
uid,
|
||||||
addr,
|
addr,
|
||||||
out,
|
out,
|
||||||
sock: None,
|
sock: None,
|
||||||
secure: false,
|
secure,
|
||||||
certfp: None,
|
certfp,
|
||||||
local_port,
|
local_port,
|
||||||
link: false,
|
link: false,
|
||||||
outbound: false,
|
outbound: false,
|
||||||
|
|
@ -690,8 +702,10 @@ fn tls_conn(
|
||||||
.any(|g| crate::modules::connclass::ip_matches(g, &addr.ip().to_string()))
|
.any(|g| crate::modules::connclass::ip_matches(g, &addr.ip().to_string()))
|
||||||
{
|
{
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
|
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
|
||||||
|
// echo terminates TLS on this listener, so only the client address is taken
|
||||||
|
// from the header (its TLS TLVs would be redundant here).
|
||||||
let real = match crate::proxy::read_header(&mut stream) {
|
let real = match crate::proxy::read_header(&mut stream) {
|
||||||
crate::proxy::Parsed::Proxy(a) => a,
|
crate::proxy::Parsed::Proxy { addr, .. } => addr,
|
||||||
crate::proxy::Parsed::Local => addr,
|
crate::proxy::Parsed::Local => addr,
|
||||||
_ => {
|
_ => {
|
||||||
let _ = shutdown.shutdown(Shutdown::Both);
|
let _ = shutdown.shutdown(Shutdown::Both);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue