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: use original dst if authority doesnt look like local service #397

Merged
merged 4 commits into from
Feb 21, 2018

Conversation

seanmonstar
Copy link
Contributor

The proxy will check that the requested authority looks like a local service, and if it doesn't, it will no longer ask the Destination service about the request, instead just using the SO_ORIGINAL_DST, enabling egress naturally.

The rules used to determine if it looks like a local service come from this comment:

If default_zone.is_none() and the name is in the form $a.$b.svc, or if !default_zone.is_none() and the name is in the form $a.$b.svc.$default_zone, for some a and some b, then use the Destination service. Otherwise, use the IP given.

@olix0r olix0r added the review/ready Issue has a reviewable PR label Feb 20, 2018
Signed-off-by: Sean McArthur <sean@seanmonstar.com>
&input, default_namespace, default_zone));
}

none("name", None, None);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tests show the effects, but I'll group them up here. These are all considered external names:

  • name (no namespace, svc, or zone)
  • name.namespace.svc.cluster (zone doesn't match cluster.local)
  • name.namespace.foo (not svc)

@seanmonstar seanmonstar requested review from adleong and removed request for olix0r February 20, 2018 21:18
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::DiscoverError> {
match *self {
Discovery::LocalSvc(ref mut w) => w.poll(),
Discovery::External(ref mut opt) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A side effect here that an external service will get inserted into the balancer, and then probably never removed. It seems like once we have some circuit breaking, this would take care of itself when/if the service closes, and outbound never requests this address again...

Copy link
Contributor

Choose a reason for hiding this comment

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

Please add your GitHub comment here as a TODO comment in the source code.

Signed-off-by: Sean McArthur <sean@seanmonstar.com>
Copy link
Member

@olix0r olix0r left a comment

Choose a reason for hiding this comment

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

the approach generally looks good to me! we should get more eyes on this before merging, but nothing obvious stands out t me.

@@ -15,10 +15,10 @@ impl FullyQualifiedAuthority {
/// This assumes the authority is syntactically valid.
pub fn new(authority: &Authority, default_namespace: Option<&str>,
Copy link
Member

Choose a reason for hiding this comment

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

nit but maybe clearer to either rename or make it a module-fn? Seems surprising for new to return an option.

.and_then(|ctx| {
ctx.orig_dst_if_not_local()
});
Destination::External(orig_dst?)
Copy link
Member

Choose a reason for hiding this comment

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

whoa! i guess this option-return-sugar must have landed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@olix0r olix0r added this to the Conduit 0.3 milestone Feb 20, 2018
@@ -111,6 +111,10 @@ fn run(proxy: Proxy) -> Listening {
env.put(config::ENV_PUBLIC_LISTENER, "tcp://127.0.0.1:0".to_owned());
env.put(config::ENV_CONTROL_LISTENER, "tcp://127.0.0.1:0".to_owned());

env.put(config::ENV_POD_NAMESPACE, "conduit".to_owned());
Copy link
Contributor

Choose a reason for hiding this comment

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

In general the pod namespace won't be "conduit". I suggest we use a different one like "foo". We should only use namespace = "conduit" for the case where we're specifically testing how the proxy works when it is proxying stuff in the "conduit" namespace (e.g., how the proxy proxies the conduit control plane). I suggest using "arbitrary" or similar instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned in a comment further down, this was done just to support the existing tests in proxy/tests/*.rs. I'll change this test and cluster.local.

@@ -15,10 +15,10 @@ impl FullyQualifiedAuthority {
/// This assumes the authority is syntactically valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

Update the documentation to mention what it means for None to be returned.

} else {
// if "a.b.svc.foo" and zone is not "foo",
// treat as external
return None;
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the early return style:

// if "a.b.svc.foo" and zone is not "foo" treat as external
if !zone.eq_ignore_ascii_case(default_zone) {
    return None;
}

}
let mut parts = name.split('.');

// parts = (name, namespace, svc, zone)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice, I didn't know about splitn().

Let's remove this comment because it isn't necessarily the case that parts is of the form (name, namespace, svc, zone). The rest of the function spends most of its effort finding out whether or not that is true.

@@ -60,11 +62,27 @@ impl FullyQualifiedAuthority {
(has_namespace, has_namespace)
};


Copy link
Contributor

Choose a reason for hiding this comment

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

To simplify the control flow below, use early return style above:

// if not a "*.svc", treat as external
if !part.eq_ignore_ascii_case("svc") {
    return None;
}

In particular, we no longer need has_svc.

let dest = if let Some(local) = local {
Destination::LocalSvc(local)
} else {
let orig_dst = req.extensions()
Copy link
Contributor

@briansmith briansmith Feb 20, 2018

Choose a reason for hiding this comment

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

Why do we look up orig_dst_if_not_local() here instead of in bind_service() below? Regardless of where we do it, let's add a comment explaining why we do it where we do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We already know at this moment whether the resolution can succeed. Rather, we know at this moment whether it's possible for this request to succeed at all, or if we definitely cannot recognize it. It seems like the error is a recognize error, not a bind error.

))
},
Destination::External(addr) => {
Discovery::External(Some((addr, self.bind.clone().with_protocol(protocol))))
Copy link
Contributor

Choose a reason for hiding this comment

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

Extending my previous comment, if we use the Destination service then we resolve a name to an IP address here, but if we use don't then we did the resolution earlier. This asymmetry is surprising.

.and_then(|ctx| {
ctx.orig_dst_if_not_local()
});
Destination::External(orig_dst?)
Copy link
Contributor

Choose a reason for hiding this comment

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

It is wrong to fail here. If we can't avoid failing here in this PR then let's add a TODO here acknowledging the problem. (Presumably this problem will be fixed when we implement DNS lookups.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see why it is wrong to fail here. If we have nothing that will allow us to route the request (recognize, as this method is called), then there's nothing to be done but to return an error.

This is also what is done in Inbound.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's wrong to fail here because, without the proxy, the request wouldn't have failed. In practice, we expect this kind of failure to never occur because we expect the proxy to only run on Linux and we expect SO_ORIGINAL_DST to work. Also, in the future we'll be able to make it work by doing a DNS lookup to get the address.

fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::DiscoverError> {
match *self {
Discovery::LocalSvc(ref mut w) => w.poll(),
Discovery::External(ref mut opt) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add your GitHub comment here as a TODO comment in the source code.

@@ -111,6 +111,10 @@ fn run(proxy: Proxy) -> Listening {
env.put(config::ENV_PUBLIC_LISTENER, "tcp://127.0.0.1:0".to_owned());
env.put(config::ENV_CONTROL_LISTENER, "tcp://127.0.0.1:0".to_owned());

env.put(config::ENV_POD_NAMESPACE, "conduit".to_owned());
env.put(config::ENV_POD_ZONE, "local".to_owned());
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean for this to be "cluster.local", which is what we normally expect things to be? If not, then it would be useful to add a comment about why you chose "local" specifically. If you just want an arbitrary value then use "example" instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually just chose those values since all the existing tests in proxy/tests use the authority test.conduit.local.

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, those tests are using "local" to make sure we don't match just "local" instead of "cluster.local". If they're using "conduit" then that's a mistake on my part.

Signed-off-by: Sean McArthur <sean@seanmonstar.com>
@seanmonstar
Copy link
Contributor Author

Review comments should be addressed. Do the exact rules of is a local vs external service seem correct?

Copy link
Contributor

@briansmith briansmith left a comment

Choose a reason for hiding this comment

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

LGTM as long as the behavior of whether we coalesce HTTP/1 requests with different Host header field values isn't different than before. Please address the nits before merging.

if !part.eq_ignore_ascii_case("svc") {
// if not "$name.$namespace.svc", treat as external
return None;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid else after return.

} else if has_explicit_namespace {
true
} else if namespace_to_append.is_none() {
// if not "*.svc" and no default namespace, treat as external
Copy link
Contributor

Choose a reason for hiding this comment

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

To be precise the comment would have to be If not "*.svc" and there's no namespace, treat as external. However, that's just duplicating the logic in the if {} else {}. I suggest the comment be // We can't append ".svc" without a namespace, so treat as external.

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Destination {
LocalSvc(FullyQualifiedAuthority),
External(SocketAddr),
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. How do we know the server is HTTP/1.1 and not HTTP/1.0? We would have had to have previously received a HTTP/1.1 response from the server to know that; we can't know it from looking at the request.

  2. Although I mostly agree with you w.r.t. what was intended in HTTP/1.1, many HTTP/1.1 servers don't properly respect Host: beyond the first request, which is why browsers don't (IIRC) send requests for different Hosts on the same connection. This is why, in particular, browsers don't do connection coalescing for HTTP/1.1, but only for HTTP/2.

Regardless, you don't have to answer those questions here. I just want to point out that it is important to not regress in this respect. As long as you're sure that the behavior isn't changed from before, we can deal with it in another issue.

.and_then(|ctx| {
ctx.orig_dst_if_not_local()
});
Destination::External(orig_dst?)
Copy link
Contributor

Choose a reason for hiding this comment

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

It's wrong to fail here because, without the proxy, the request wouldn't have failed. In practice, we expect this kind of failure to never occur because we expect the proxy to only run on Linux and we expect SO_ORIGINAL_DST to work. Also, in the future we'll be able to make it work by doing a DNS lookup to get the address.

Signed-off-by: Sean McArthur <sean@seanmonstar.com>
@seanmonstar seanmonstar merged commit 236f71f into master Feb 21, 2018
@seanmonstar seanmonstar removed the review/ready Issue has a reviewable PR label Feb 21, 2018
@seanmonstar seanmonstar deleted the proxy-egress branch February 21, 2018 02:09
khappucino pushed a commit to Nordstrom/linkerd2 that referenced this pull request Mar 5, 2019
…inkerd#397)

The proxy will check that the requested authority looks like a local service, and if it doesn't, it will no longer ask the Destination service about the request, instead just using the SO_ORIGINAL_DST, enabling egress naturally.

The rules used to determine if it looks like a local service come from this comment:

> If default_zone.is_none() and the name is in the form $a.$b.svc, or if !default_zone.is_none() and the name is in the form $a.$b.svc.$default_zone, for some a and some b, then use the Destination service. Otherwise, use the IP given.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants