Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add inbound support for svc hostname in authority #1420

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
95 changes: 95 additions & 0 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ pub enum Error {
#[error("unknown waypoint: {0}")]
UnknownWaypoint(String),

#[error("no service or workload for hostname: {0}")]
NoHostname(String),

#[error("no valid authority pseudo header: {0}")]
NoValidAuthority(String),

#[error("no valid routing destination for workload: {0}")]
NoValidDestination(Box<Workload>),

Expand Down Expand Up @@ -485,19 +491,27 @@ pub enum Error {
DnsEmpty,
}

// Custom TLV for proxy protocol for the identity of the source
const PROXY_PROTOCOL_AUTHORITY_TLV: u8 = 0xD0;
// Custom TLV for including the svc hostname in the proxy protocol header
const PROXY_PROTOCOL_SVC_HOSTNAME_TLV: u8 = 0xD1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is an interesting one. I don't see any strong reason not to do this other than I don't know of any use cases to do so, and if we do we probably can never remove it since its part of our runtime API contract.

Did you have a specific use in mind for this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, I imagine that if there's custom waypoint sandwiched with the ztunnel, it would want to use the hostname for routing (vs. an IP address we lookup) , right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keithmattix correct me if I am wrong here. We were concerned about not preserving the hostname in the proxy protocol. Not positive about the exact implications of this for logging, but maybe it has more implications for security tools?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah logging is one thing, but I'm really more thinking about fidelity. The sandwiched proxy should use the data on the wire as much as possible rather than an IP that ztunnel provides based on state that it may have on hand


pub async fn write_proxy_protocol<T>(
stream: &mut TcpStream,
addresses: T,
src_id: Option<Identity>,
hostname_addr: Option<Strng>,
) -> io::Result<()>
where
T: Into<ppp::v2::Addresses> + std::fmt::Debug,
{
use ppp::v2::{Builder, Command, Protocol, Version};
use tokio::io::AsyncWriteExt;

// When the hbone_addr populated from the authority header contains a svc hostname, the address included
// with respect to the hbone_addr is the SocketAddr <dst svc IP>:<original dst port>.
// This is done since addresses doesn't support hostnames.
// See ref https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
debug!("writing proxy protocol addresses: {:?}", addresses);
let mut builder =
Builder::with_addresses(Version::Two | Command::Proxy, Protocol::Stream, addresses);
Expand All @@ -506,6 +520,11 @@ where
builder = builder.write_tlv(PROXY_PROTOCOL_AUTHORITY_TLV, id.to_string().as_bytes())?;
}

// svc_hostname is None when the hbone_addr does not contain a svc hostname.
if let Some(svc_host) = hostname_addr {
builder = builder.write_tlv(PROXY_PROTOCOL_SVC_HOSTNAME_TLV, svc_host.as_bytes())?;
}

let header = builder.build()?;
stream.write_all(&header).await
}
Expand Down Expand Up @@ -782,6 +801,82 @@ pub fn parse_forwarded_host(input: &str) -> Option<String> {
.filter(|host| !host.is_empty())
}

#[derive(Debug, Clone)]
pub enum HboneAddress {
SocketAddr(SocketAddr),
SvcHostname(Strng, u16),
}

impl HboneAddress {
pub fn port(&self) -> u16 {
match self {
HboneAddress::SocketAddr(s) => s.port(),
HboneAddress::SvcHostname(_, p) => *p,
}
}

pub fn ip(&self) -> Option<IpAddr> {
match self {
HboneAddress::SocketAddr(s) => Some(s.ip()),
HboneAddress::SvcHostname(_, _) => None,
}
}

pub fn svc_hostname(&self) -> Option<Strng> {
match self {
HboneAddress::SocketAddr(_) => None,
HboneAddress::SvcHostname(s, _) => Some(s.into()),
}
}

pub fn hostname_addr(&self) -> Option<Strng> {
match self {
HboneAddress::SocketAddr(_) => None,
HboneAddress::SvcHostname(_, _) => Some(Strng::from(self.to_string())),
}
}
}

impl std::fmt::Display for HboneAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HboneAddress::SocketAddr(addr) => write!(f, "{}", addr),
HboneAddress::SvcHostname(host, port) => write!(f, "{}:{}", host, port),
}
}
}

impl From<SocketAddr> for HboneAddress {
fn from(socket_addr: SocketAddr) -> Self {
HboneAddress::SocketAddr(socket_addr)
}
}

impl From<(Strng, u16)> for HboneAddress {
fn from(svc_hostname: (Strng, u16)) -> Self {
HboneAddress::SvcHostname(svc_hostname.0, svc_hostname.1)
}
}

impl TryFrom<&http::Uri> for HboneAddress {
type Error = Error;

fn try_from(value: &http::Uri) -> Result<Self, Self::Error> {
match value.to_string().parse::<SocketAddr>() {
Ok(addr) => Ok(HboneAddress::SocketAddr(addr)),
Err(_) => {
let hbone_host = value
.host()
.ok_or_else(|| Error::NoValidAuthority(value.to_string()))?;
let hbone_port = value
.port_u16()
.ok_or_else(|| Error::NoValidAuthority(value.to_string()))?;
Ok(HboneAddress::SvcHostname(hbone_host.into(), hbone_port))
}
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading