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 10 commits into
base: master
Choose a base branch
from

Conversation

jaellio
Copy link

@jaellio jaellio commented Jan 10, 2025

Hoping to get feedback on the overall approach for this change. I would appreciate any rust specific feedback as well.

PR still needs some minor refactoring.

Main changes:

  • hbone_address supports hostname or ip when reading from the authority header
  • when a hostname is included in the authority pseudo header in is included as header in the proxy protocol. The hbone address included in the address header is set to the destination service vip
  • hostname is validated by looking up the respective service and verifying the destination pod ip is a member of that service.

Todo:

  • refactor hbone address validation to improve testability
  • update error types
  • confirm if port should also be included with hostname in custom protocol type
  • intelligently select service vip for proxy protocol address header
  • strictly validate hostname fqdn

@istio-testing
Copy link
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@istio-testing istio-testing added the do-not-merge/work-in-progress Block merging of a PR because it isn't ready yet. label Jan 10, 2025
@istio-policy-bot
Copy link

😊 Welcome @jaellio! This is either your first contribution to the Istio ztunnel repo, or it's been
a while since you've been here.

You can learn more about the Istio working groups, Code of Conduct, and contribution guidelines
by referring to Contributing to Istio.

Thanks for contributing!

Courtesy of your friendly welcome wagon.

@istio-testing istio-testing added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Jan 10, 2025
@jaellio
Copy link
Author

jaellio commented Jan 10, 2025

/test all

// TODO(jaellio): Should we also validate the hbone_addr port?
HboneAddress::SvcHostname(hbone_host, hbone_port) => {
// Validate a service or workload exists for the hostname
let parts: Vec<&str> = hbone_host.split('.').collect();
Copy link
Author

Choose a reason for hiding this comment

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

TODO - figure out if there is a better way to parse a hostname

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can. There is not really a mapping from hostname -> namespace. For example, I could have a hostname foo.com. We could have a guess or heuristic, but this doesn't seem like a time when doing the check only some of the time provides much value?

Copy link
Member

Choose a reason for hiding this comment

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

Since its to look up the service, perhaps we do something like find all the services with the hostname and pick the first preferring one with the same namespace (as the workload) if available

Signed-off-by: Jackie Elliott <[email protected]>
@jaellio
Copy link
Author

jaellio commented Jan 10, 2025

/test test

@jaellio
Copy link
Author

jaellio commented Jan 10, 2025

/test test

Signed-off-by: Jackie Elliott <[email protected]>
src/proxy/inbound.rs Outdated Show resolved Hide resolved
src/proxy/inbound.rs Outdated Show resolved Hide resolved
src/proxy/inbound.rs Outdated Show resolved Hide resolved
src/proxy/inbound.rs Outdated Show resolved Hide resolved
src/proxy/inbound.rs Outdated Show resolved Hide resolved

// Determine the next hop.
let hbone_addr_clone = hbone_addr.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

Making hbone_addr an Arc may help with the clones

Copy link
Contributor

@ilrudie ilrudie Jan 15, 2025

Choose a reason for hiding this comment

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

I think it could probably be changed so that find_inbound_upstream takes a borrow. Not sure why it accepts so many borrows and one owned TBH. All we really care about here is the port I think which is cheap to copy on the stack vs a clone of something more complex

HboneAddress::SvcHostname(hbone_host, hbone_port) => {
// Validate a service or workload exists for the hostname
let parts: Vec<&str> = hbone_host.split('.').collect();
if parts.len() <= 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can probably be a little stronger here and require an FQDN (i.e. error if we detect a short name with less than 5 parts). I think Istio always requires FQDNs for multi-cluster addressing (/cc @howardjohn)

Copy link
Author

Choose a reason for hiding this comment

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

Agree. Kept it simple for the draft but this isn't sufficient atm.

Copy link
Member

Choose a reason for hiding this comment

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

Yes to require FQDN. However I am not convinced we can/should do the mapping of service --> namespace at all (see comment 2 lines up)

src/proxy/inbound.rs Outdated Show resolved Hide resolved
hbone_addr: SocketAddr,
) -> Result<(), Error> {
hbone_addr: HboneAddress,
) -> Result<Option<SocketAddr>, Error> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't love the idea of returning a value from this function. I know you need it for the proxy protocol stuff so maybe the initial hbone_addr matching can be a part of a separate function that's called only if the :authority is a hostname

Copy link
Author

Choose a reason for hiding this comment

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

Agree, just a temporary hack. Will fix

Signed-off-by: Jackie Elliott <[email protected]>
@jaellio jaellio marked this pull request as ready for review January 14, 2025 23:28
@jaellio jaellio requested a review from a team as a code owner January 14, 2025 23:28
@istio-testing istio-testing removed the do-not-merge/work-in-progress Block merging of a PR because it isn't ready yet. label Jan 14, 2025
StatusCode::BAD_REQUEST,
));
}
// TODO(jaellio): Intelligently select the VIP
Copy link
Author

Choose a reason for hiding this comment

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

@keithmattix with regards to #1409 and intelligently selecting vip for a Address::Service, is it sufficient to rank vips by network comparison (higher rank if vip.network matches conn.dst_network) or should we also be comparing IpAddr ranges? Do we also want to implement ranking of Address::Services when finding hostnames (similar to how there would be duplicate workloads on lookup by IP)?

@jaellio jaellio added the do-not-merge/hold Block automatic merging of a PR. label Jan 15, 2025
Copy link
Member

@howardjohn howardjohn left a comment

Choose a reason for hiding this comment

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

A

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?

@@ -262,38 +346,90 @@ impl Inbound {
}

let start = Instant::now();

// Extract the host or IP from the authority pseudo-header of the URI
let hbone_addr = req
Copy link
Member

Choose a reason for hiding this comment

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

nit: you could make this a

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

    fn try_from(value: &http::Uri) -> Result<Self, Self::Error> { ... }

Then the calling code is just req.uri().try_into()

Comment on lines +357 to +358
let hbone_host = req.uri().host().unwrap();
let hbone_port = req.uri().port_u16().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

I don't think these unwraps are safe. I suspect :authority: foo (no port) would panic here for example. Which we want to reject (probably with a BAD_REQUEST), but shouldn't unwrap.

@@ -52,6 +53,56 @@ pub(super) struct Inbound {
enable_orig_src: bool,
}

#[derive(Debug, Clone)]
pub enum HboneAddress {
Copy link
Member

Choose a reason for hiding this comment

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

nit: not a big deal but given this is used outside inbound might make sense to put it up in proxy module instead

// TODO(jaellio): Intelligently select the VIP
let svc_address = svc.vips[0].address;
upstream_protocol_addr = format!("{}:{}", svc_address, hbone_addr.port())
.parse::<SocketAddr>()
Copy link
Member

Choose a reason for hiding this comment

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

nit: No need to send to string and parse back, SocketAddr::new(svc_address, hbone_addr.port()) should work

// TODO(jaellio): Should we also validate the hbone_addr port?
HboneAddress::SvcHostname(hbone_host, hbone_port) => {
// Validate a service or workload exists for the hostname
let parts: Vec<&str> = hbone_host.split('.').collect();
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can. There is not really a mapping from hostname -> namespace. For example, I could have a hostname foo.com. We could have a guess or heuristic, but this doesn't seem like a time when doing the check only some of the time provides much value?

HboneAddress::SvcHostname(hbone_host, hbone_port) => {
// Validate a service or workload exists for the hostname
let parts: Vec<&str> = hbone_host.split('.').collect();
if parts.len() <= 1 {
Copy link
Member

Choose a reason for hiding this comment

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

Yes to require FQDN. However I am not convinced we can/should do the mapping of service --> namespace at all (see comment 2 lines up)

// TODO(jaellio): Should we also validate the hbone_addr port?
HboneAddress::SvcHostname(hbone_host, hbone_port) => {
// Validate a service or workload exists for the hostname
let parts: Vec<&str> = hbone_host.split('.').collect();
Copy link
Member

Choose a reason for hiding this comment

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

Since its to look up the service, perhaps we do something like find all the services with the hostname and pick the first preferring one with the same namespace (as the workload) if available

local_workload: &Workload,
hbone_addr: HboneAddress,
) -> Result<Address, Error> {
match hbone_addr {
Copy link
Member

Choose a reason for hiding this comment

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

Its a bit of a codesmell to have the function take in an enum but then require a specific variant is passed.

In this specific case its pretty easy to resolve though, instead of taking in HboneAddress we can just take in a Strng and have them pass in the hostname.

// Normal case: both are aligned. This is allowed (we really only need the HBONE address for the port.)
let hbone_ip = match hbone_addr.ip() {
Some(ip) => ip,
None => return Err(Error::ConnectAddress(hbone_addr.to_string())),
Copy link
Member

Choose a reason for hiding this comment

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

Its a bit odd to have validate_destination only called for IP, then reject non-IP. Maybe we can just call it in all cases. if we have nothing to validate for hostname cases for now (which seems reasonable and fine) then we can just return Ok(()) immediately if its a hostname

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do-not-merge/hold Block automatic merging of a PR. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add service hostname HBONE :authority support to ztunnel servers
7 participants