Conversation
This change adds a standalone utility crate that will support matching routes for the Gateway API's HTTP route types. This crate is not yet used. In followup changes: * filter types will be added to support header-rewriting, HTTP redirection, and error injection filters; * the inbound proxy will be updated to supcort route-oriented authorization policies; and * the inbound policy API client will be updated to configure inbound server routes. Signed-off-by: Oliver Gould <[email protected]>
This change adds per-route authorization policies to the inbound proxy. This change does NOT introduce an API client to configure these policies. That will be added in follow-up changes, as will richer per-route configurations. This change: * Modifies the inbound policy `Protocol` type so that each protocol variant includes policy configuration. TCP protocols now include authorization policies (moved from the `Server` type). * Implements an HTTP policy router that determines the route for each request and applies its authorization policy. * Fails requests for which no route matches with a `404 Not Found` response. A new metric, `inbound_http_route_not_found_total`, is introduced to count occurrences of this error. * Includes a default route for all inbound HTTP communication, using the server's authorization policies. Signed-off-by: Oliver Gould <[email protected]>
This change adds several route filters: * An HTTP header rewriting; * An HTTP redirection filter; * An HTTP error filter; and * A gRPC error filter. The header-rewriting and HTTP redirection filters are implemented as specified by the Kubernetes Gateway API HTTPRoute spec. These filters apply only on inbound policies are not yet configurable via the control plane. A followup change will be made to support configuring these filters. Depends on #1781. Signed-off-by: Oliver Gould <[email protected]>
This change adds per-route authorization policies to the inbound proxy. This change does NOT introduce an API client to configure these policies. That will be added in follow-up changes, as will richer per-route configurations. This change: * Modifies the inbound policy `Protocol` type so that each protocol variant includes policy configuration. TCP protocols now include authorization policies (moved from the `Server` type). * Implements an HTTP policy router that determines the route for each request and applies its authorization policy. * Fails requests for which no route matches with a `404 Not Found` response. A new metric, `inbound_http_route_not_found_total`, is introduced to count occurrences of this error. * Includes a default route for all inbound HTTP communication, using the server's authorization policies. Signed-off-by: Oliver Gould <[email protected]>
1d0df3f to
10cb66e
Compare
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
Signed-off-by: Oliver Gould <[email protected]>
The Gateway API defines a [header-rewriting filter](gwapi) that may be attached to HTTP routes. This change updates the HTTP route types to support a list of filters. The inbound proxy supports only the header modifier filter. Additional filters will be added in follow-up changes (see #1788 for an example of these additional filters). [gwapi]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRequestHeaderFilter Signed-off-by: Oliver Gould <[email protected]>
The Gateway API defines a [header-rewriting filter](gwapi) that may be attached to HTTP routes. This change updates the HTTP route types to support a list of filters. The inbound proxy supports only the header modifier filter. Additional filters will be added in follow-up changes (see #1788 for an example of these additional filters). [gwapi]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRequestHeaderFilter Signed-off-by: Oliver Gould <[email protected]>
The Gateway API defines a [header-rewriting filter](gwapi) that may be attached to HTTP routes. This change updates the HTTP route types to support a list of filters. The inbound proxy supports only the header modifier filter. Additional filters will be added in follow-up changes (see #1788 for an example of these additional filters). [gwapi]: https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRequestHeaderFilter Signed-off-by: Oliver Gould <[email protected]>
hawkw
left a comment
There was a problem hiding this comment.
i got kind of nerd-sniped by trying to reduce allocation & copying in the RedirectRequest's URI-rewriting code, so i left a bunch of comments there. if you like, i'd be happy to open a PR against this with what i believe would be a more efficient implementation, but it's also something we can come back to later.
| } | ||
|
|
||
| Ok(None) => { | ||
| tracing::debug!("Ignoring irrelvant redirect"); |
There was a problem hiding this comment.
typo:
| tracing::debug!("Ignoring irrelvant redirect"); | |
| tracing::debug!("Ignoring irrelevant redirect"); |
| (Some(h), p) => p | ||
| .map(|p| format!("{}:{}", h, p).parse()) | ||
| .unwrap_or_else(|| h.parse())?, |
There was a problem hiding this comment.
seems like we could potentially avoid doing this for every request, in the case that there's a port configured --- this authority doesn't depend on the request at all, unlike in the case where the redirect is configured without a host...
| .map(|p| format!("{}:{}", h, p).parse()) | ||
| .unwrap_or_else(|| h.parse())?, | ||
| // If a host is NOT configured, use the request's original host and either an | ||
| // overridden port or the original port. | ||
| (None, p) => { | ||
| let h = orig_uri.host().ok_or(InvalidRedirect::MissingAuthority)?; | ||
| p.or_else(|| orig_uri.port_u16()) | ||
| .map(|p| format!("{}:{}", h, p).parse()) | ||
| .unwrap_or_else(|| h.parse())? |
There was a problem hiding this comment.
also, we may want to use the TryFrom<String> impl for Authority rather than FromStr? the TryFrom<String> impl will try to construct an Authority from the String converted into a Bytes while the FromStr impl calls the TryFrom<&'a str> impl, which will copy the &str's bytes into a new Bytes. since we have ownership of the strings returned by format! here, we may want to use the conversion that takes the String by value, as it won't allocate a new Bytes and copy the string into it, but instead uses the same allocation.
| Some(ModifyPath::ReplacePrefixMatch(new_pfx)) => match rm.route.path() { | ||
| PathMatch::Prefix(pfx_len) if *pfx_len <= orig_path.len() => { | ||
| let (_, rest) = orig_path.split_at(*pfx_len); | ||
| format!("{}{}", new_pfx, rest) |
There was a problem hiding this comment.
it might be a little bit more efficient to do something like
| format!("{}{}", new_pfx, rest) | |
| let mut path = String::with_capacity(new_pfx.len() + rest.len()); | |
| path.push_str(new_pfx); | |
| path.push_str(rest); | |
| path |
but, that's also several more lines of code...
| let orig_path = orig_uri.path(); | ||
| match &self.path { | ||
| None => orig_path.to_string(), | ||
| Some(ModifyPath::ReplaceFullPath(p)) => p.clone(), |
There was a problem hiding this comment.
if we stored the full path as a Bytes rather than a String, the clone here would just be a reference count bump, rather than an alloc + memcpy (and http::uri::Builder::path_and_query will accept a Bytes). we would also have to convert the modified path in that case, though.
| let path = { | ||
| use crate::http::r#match::PathMatch; | ||
|
|
||
| let orig_path = orig_uri.path(); | ||
| match &self.path { | ||
| None => orig_path.to_string(), | ||
| Some(ModifyPath::ReplaceFullPath(p)) => p.clone(), | ||
| Some(ModifyPath::ReplacePrefixMatch(new_pfx)) => match rm.route.path() { | ||
| PathMatch::Prefix(pfx_len) if *pfx_len <= orig_path.len() => { | ||
| let (_, rest) = orig_path.split_at(*pfx_len); | ||
| format!("{}{}", new_pfx, rest) | ||
| } |
There was a problem hiding this comment.
if we converted the paths in the other match arms to http::uri::PathAndQuery here, we could re-use the original request path without formatting it to a string and then back again:
| let path = { | |
| use crate::http::r#match::PathMatch; | |
| let orig_path = orig_uri.path(); | |
| match &self.path { | |
| None => orig_path.to_string(), | |
| Some(ModifyPath::ReplaceFullPath(p)) => p.clone(), | |
| Some(ModifyPath::ReplacePrefixMatch(new_pfx)) => match rm.route.path() { | |
| PathMatch::Prefix(pfx_len) if *pfx_len <= orig_path.len() => { | |
| let (_, rest) = orig_path.split_at(*pfx_len); | |
| format!("{}{}", new_pfx, rest) | |
| } | |
| let path: http::uri::PathAndQuery = { | |
| use crate::http::r#match::PathMatch; | |
| let orig_path = orig_uri.path(); | |
| match &self.path { | |
| None => orig_path, | |
| Some(ModifyPath::ReplaceFullPath(p)) => p.clone().try_into()?, | |
| Some(ModifyPath::ReplacePrefixMatch(new_pfx)) => match rm.route.path() { | |
| PathMatch::Prefix(pfx_len) if *pfx_len <= orig_path.len() => { | |
| let (_, rest) = orig_path.split_at(*pfx_len); | |
| format!("{}{}", new_pfx, rest).try_into()? | |
| } |
we would need to change the error a bit in that case, though, because the String -> PathAndQuery conversion also returns InvalidUrl
Signed-off-by: Oliver Gould <[email protected]>
|
BTW, I did end up throwing together a quick branch that makes some optimizations to the redirect URI rewriting; happy to open a PR against this branch, or save it for later if you're still making a bunch of changes here. |
|
@hawkw For what it's worth, I don't think it's particularly important to optimize this codepath. I'd bias strongly towards readability for the time being. |
Signed-off-by: Oliver Gould <[email protected]>
| let path = { | ||
| use crate::http::r#match::PathMatch; | ||
|
|
||
| let orig_path = orig_uri.path(); | ||
| match &self.path { | ||
| None => orig_path.to_string(), | ||
| Some(ModifyPath::ReplaceFullPath(p)) => p.clone(), | ||
| Some(ModifyPath::ReplacePrefixMatch(new_pfx)) => match rm.route.path() { | ||
| PathMatch::Prefix(pfx_len) if *pfx_len <= orig_path.len() => { | ||
| let (_, rest) = orig_path.split_at(*pfx_len); | ||
| format!("{}{}", new_pfx, rest) | ||
| } | ||
| _ => return Err(InvalidRedirect::InvalidReplacePrefix), | ||
| }, | ||
| } | ||
| }; |
There was a problem hiding this comment.
Noticed what I believe is a bug here: in all cases (even when just using the original path), this filter will clobber any query params. This is because we get orig_path by calling orig_uri.path(), which returns just the path component off the URI, and not the query component, but then we pass a value constructed using orig_path to uri::Builder::path_and_query when constructing the new URI. If the original URL had a query component, it's discarded here.
I think we should probably call orig_uri.query() at some point and ensure that the query parameters are added to the new path-and-query part passed to uri::Builder::path_and_query.
There was a problem hiding this comment.
(as a side note, it also appears that the redirect filter will currently always throw out any fragment identifier component of the URI...but, see also hyperium/http#127)
This change adds several route filters:
The header-rewriting and HTTP redirection filters are implemented as
specified by the Kubernetes Gateway API HTTPRoute spec.
These filters apply only on inbound policies are not yet configurable
via the control plane. A followup change will be made to support
configuring these filters. Depends on #1781.
Signed-off-by: Oliver Gould [email protected]