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

proxy: Clarify Outbound::recognize #1144

Merged
merged 8 commits into from
Jun 19, 2018
Merged
Changes from 3 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
108 changes: 71 additions & 37 deletions proxy/src/outbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,74 @@ impl<B> Outbound<B> {
bind_timeout,
}
}


/// TODO: Return error when `HostAndPort::normalize()` fails.
Copy link
Member

Choose a reason for hiding this comment

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

If we're discarding the error here, maybe we should at least .inspect_err(|e| error!("normalizing {:?} failed: {:?}", authority, e)) first (or something like that)?

I know the original code you're refactoring didn't do that, but it seems simple enough that we could add it in this PR...

Copy link
Member Author

Choose a reason for hiding this comment

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

What does that do?

Copy link
Member

Choose a reason for hiding this comment

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

Log the error prior to discarding it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is this part of the std lib or is it an extension? I don't see this defined on Result. In any case, let's address this separately.

Copy link
Member

Choose a reason for hiding this comment

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

Is this part of the std lib or is it an extension?

Ah, nevermind, that's a method on Stream, I think, that I mistakenly assumed was also on Results; map_err would do more or less the same thing here though, since the error value gets discarded anyway.

In any case, let's address this separately.
👍

/// TODO: Use scheme-appropriate default port.
fn normalize(authority: &http::uri::Authority) -> Option<HostAndPort> {
const DEFAULT_PORT: Option<u16> = Some(80);
HostAndPort::normalize(authority, DEFAULT_PORT)
.inspect_err(|e| error!("invalid authority=\"{}\": {:?}", authority, e))
}

/// Determines the logical host:port of the request.
Copy link
Member

Choose a reason for hiding this comment

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

NIT: I think the preferred convention for rustdoc comments is for the summary of a function to be a present-tense description of the action that the function does, like "Determine the logical host:port..." rather than "Determines..."

Copy link
Member

Choose a reason for hiding this comment

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

(also I think the bikeshed should be blue...)

Copy link
Member Author

Choose a reason for hiding this comment

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

@hawkw From everything I've seen, the idiomatic style is to conjugate them as I did here.

https://doc.rust-lang.org/src/core/convert.rs.html#309
https://doc.rust-lang.org/src/alloc/vec.rs.html#1091
etc

Copy link
Member

Choose a reason for hiding this comment

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

@olix0r Yeah, it looks like TRPL also uses that conjugation, color me wrong then!

I'm not sure where that got into my head from...

///
/// If the parsed URI includes an authority, use that. Otherwise, try to load the
/// authority from the `Host` header.
///
/// The port is either parsed from the authority or a default of 80 is used.
Copy link
Member

Choose a reason for hiding this comment

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

Splitting these out is great, and I love the added documentation.

fn host_port(req: &http::Request<B>) -> Option<HostAndPort> {
// Note: Calls to `normalize` cannot be deduped without cloning `authority`.
req.uri()
.authority_part()
.and_then(Self::normalize)
.or_else(|| {
h1::authority_from_host(req)
.and_then(|h| Self::normalize(&h))
})
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems like we could do req.uri().authority_part().or_else(|| h1::authority_from_req(req)).and_then(Self::normalize)) and that would be better, if we can. (I'm guessing there's some reason we can't.)

Copy link
Member Author

Choose a reason for hiding this comment

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

To do this, we have to clone the authority. (because authority_part returns a ref, and we can't make the or_else block return a ref in your suggestion). We only need a ref, though, so preferred not to clone just to pass the ref.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, sounds good!

}

/// Determines the destination to use in the router.
///
/// A Destination is determined for each request as follows:
///
/// 1. If the request includes a logical authority, it is routed by the hostname and
/// port. If an explicit port is not provided, a default is assumed.
///
/// 2. If the request authority contains a concrete IP address, it is routed directly
/// to that IP address.
///
/// 3. If no authority is present on the request, the client's original destination,
/// as determined by the SO_ORIGINAL_DST socket option, is used as the request's
/// destination.
///
/// The SO_ORIGINAL_DST socket option is typically set by iptables(8) in
/// containerized environments like Kubernetes (as configured by the proxy-init
/// program).
///
/// 4. If the request has no authority and the SO_ORIGINAL_DST socket option has not
/// been set (or is unavailable on the current platform), no destination is
/// determined.
fn destination(req: &http::Request<B>) -> Option<Destination> {
match Self::host_port(req) {
Some(HostAndPort { host: Host::DnsName(host), port }) => {
let dst = DnsNameAndPort { host, port };
Some(Destination::Hostname(dst))
}

Some(HostAndPort { host: Host::Ip(ip), port }) => {
let dst = SocketAddr::from((ip, port));
Some(Destination::ImplicitOriginalDst(dst))
Copy link
Contributor

Choose a reason for hiding this comment

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

ImplicitOriginalDst is a very misleading name for this now. We should name it Destination::Ip.

}

None => {
req.extensions()
.get::<Arc<ctx::transport::Server>>()
.and_then(|ctx| ctx.orig_dst_if_not_local())
.map(Destination::ImplicitOriginalDst)
}
}
}
}

impl<B> Clone for Outbound<B>
Expand Down Expand Up @@ -88,45 +156,11 @@ where
choose::PowerOfTwoChoices,
>>>>;

// Route the request by its destination AND PROTOCOL. This prevents HTTP/1
// requests from being routed to HTTP/2 servers, and vice versa.
fn recognize(&self, req: &Self::Request) -> Option<Self::Key> {
let dest = Self::destination(req)?;
let proto = bind::Protocol::detect(req);

// The request URI and Host: header have not yet been normalized
// by `NormalizeUri`, as we need to know whether the request will
// be routed by Host/authority or by SO_ORIGINAL_DST, in order to
// determine whether the service is reusable.
let authority = req.uri().authority_part()
.cloned()
// Therefore, we need to check the host header as well as the URI
// for a valid authority, before we fall back to SO_ORIGINAL_DST.
.or_else(|| h1::authority_from_host(req));

// TODO: Return error when `HostAndPort::normalize()` fails.
let mut dest = match authority.as_ref()
.and_then(|auth| HostAndPort::normalize(auth, Some(80)).ok()) {
Some(HostAndPort { host: Host::DnsName(dns_name), port }) =>
Some(Destination::Hostname(DnsNameAndPort { host: dns_name, port })),
Some(HostAndPort { host: Host::Ip(_), .. }) |
None => None,
};

if dest.is_none() {
dest = req.extensions()
.get::<Arc<ctx::transport::Server>>()
.and_then(|ctx| {
ctx.orig_dst_if_not_local()
})
.map(Destination::ImplicitOriginalDst)
};

// If there is no authority in the request URI or in the Host header,
// and there is no original dst, then we have nothing! In that case,
// return `None`, which results an "unrecognized" error. In practice,
// this shouldn't ever happen, since we expect the proxy to be run on
// Linux servers, with iptables setup, so there should always be an
// original destination.
let dest = dest?;

Some((dest, proto))
}

Expand Down