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

listener: filter chain unified matchers #18871

Closed

Conversation

kyessenov
Copy link
Contributor

@kyessenov kyessenov commented Nov 2, 2021

Commit Message: Generalize filter chain match extensions and allow custom order of specificity matching.
Additional Description:
Risk Level:
Testing:
Docs Changes:
Release Notes:
Platform Specific Features:
Fixes: #3411 #18685

Signed-off-by: Kuat Yessenov <kuat@google.com>
@repokitteh-read-only
Copy link

CC @envoyproxy/api-shepherds: Your approval is needed for changes made to (api/envoy/|docs/root/api-docs/).
envoyproxy/api-shepherds assignee is @markdroth
CC @envoyproxy/api-watchers: FYI only for changes made to (api/envoy/|docs/root/api-docs/).

🐱

Caused by: #18871 was opened by kyessenov.

see: more, trace.

@mattklein123
Copy link
Member

cc @tonya11en

Signed-off-by: Kuat Yessenov <kuat@google.com>
// 8. :ref:`SourceIP <envoy_v3_api_msg_config.listener.v3.FilterChainMatch.SourceIP>`.
// 9. :ref:`SourcePort <envoy_v3_api_msg_config.listener.v3.FilterChainMatch.SourcePort>`.
//
// Filter chain match conditions that are not in the matching order list are
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'm debating whether we should perform a linear check for predicates that are not in this list instead. That seems more intuitive.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry what do you mean by this exactly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Imagine someone adds a new filter chain match predicate using the metadata. If we don't assume this order is complete, then there will be multiple matching filter chains left by default for metadata matching.

Comment on lines 135 to 137
// In case, there is more than one filter chain candidates remaining after
// the process completes, the first of the filter chains in the order of
// their declaration is selected.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a breaking change. Why do you want to relax the constraint?

My understanding is that changing match order may impact the last select chain. That's fine.

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 existing implementation does not guarantee uniqueness, right? E.g. you can have two source port ranges that are overlapping but neither is more specific.

Copy link
Contributor

Choose a reason for hiding this comment

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

It guarantees. An exception will be thrown upon before executing listener update.

I need to check the source port range, but for other match criteria, e.g. the ip range, we continue to drill down the rest matchers to confirm no 2 chains can be selected

Copy link
Contributor

Choose a reason for hiding this comment

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

Proof

  if (!source_ports_map.try_emplace(source_port, filter_chain).second) {
    // If we got here and found already configured branch, then it means that this FilterChainMatch
    // is a duplicate, and that there is some overlap in the repeated fields with already processed
    // FilterChainMatches.
    throw EnvoyException(fmt::format("error adding listener '{}': multiple filter chains with "
                                     "overlapping matching rules are defined",
                                     address_->asString()));
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mandating that matching predicates form an independent join lattice seems unnecessary. So what if there are two overlapping IP ranges? Maybe it's down-selected further in some other condition.

Copy link
Member

Choose a reason for hiding this comment

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

I tend to agree with @kyessenov though I'm wary of changing the implementation in subtle ways for the default. Is it hard to keep the existing constraint for now, at least for predicates that check this type of thing? It seems not too hard?

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'll have to look at the implementation in more detail to answer. I am not sure how to detect this statically since you can have a product {sni: ["x", "y"], source_ports: [80, 81]} and {sni: ["x", "z"], source_ports: [79, 80]}. For sni "x" and source port "80" there are two chains but it's not obvious from the definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually an important usability point. I think the desire to have only one matching chain makes it really hard to use the filter chains. The user expects empty match predicates to match everything, not nothing. So it's up to the user to structure the matches so that only one chain remains in most cases, but we should handle multiple "match-all" chains.

@mattklein123 mattklein123 self-assigned this Nov 3, 2021
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks for working on this. A few questions to get started.

/wait

// 8. :ref:`SourceIP <envoy_v3_api_msg_config.listener.v3.FilterChainMatch.SourceIP>`.
// 9. :ref:`SourcePort <envoy_v3_api_msg_config.listener.v3.FilterChainMatch.SourcePort>`.
//
// Filter chain match conditions that are not in the matching order list are
Copy link
Member

Choose a reason for hiding this comment

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

Sorry what do you mean by this exactly?

Comment on lines 135 to 137
// In case, there is more than one filter chain candidates remaining after
// the process completes, the first of the filter chains in the order of
// their declaration is selected.
Copy link
Member

Choose a reason for hiding this comment

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

I tend to agree with @kyessenov though I'm wary of changing the implementation in subtle ways for the default. Is it hard to keep the existing constraint for now, at least for predicates that check this type of thing? It seems not too hard?

message DestinationPort {
// Optional destination port to consider when use_original_dst is set on the
// listener in determining a filter chain match.
google.protobuf.UInt32Value destination_port = 1
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 are going to make this more generic anyway, can we change this to a range or list similar to source ports?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack, makes sense.

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 have a range, do we need the single port? You can always put a single port into the range definition so there is no backwards compat issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It looks rather ugly to have one element range for many chains. Majority of listeners with original_dst just want one port.

Comment on lines 304 to 309
// Specifies the filter chain matching predicates to be used in the matching
// order. Each predicate extension must be specified at most once. For
// backwards compatibility, the existing fields in the filter chain match, if
// specified, are converted to their corresponding filter chain matching
// predicates in this list.
// [#not-implemented-hide:]
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused about the purpose of this field in relation to matching_order. Don't we basically just want a list of type extensions somewhere that will be the configurable matching order? Why do we need matching order above also?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually a set not a list of predicate extension configs for each predicate in the matching order. Not sure how else to express this concept. The order here is irrelevant.

Copy link
Member

Choose a reason for hiding this comment

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

This is related to my comment above about matching order. I'm confused as to why we can't just have everything expressed in a single ordered list? Basically just force people to instantiate the list with the type urls in the order they want? The existing config can be translated to the ordered list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each filter chain has different values for the predicates. Do you suggest each filter chain must have the same order for type URLs and then we deduce the global order from that? That makes sense.

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 think one annoyance is that empty predicates must be specified.

Copy link
Member

Choose a reason for hiding this comment

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

OK I see the trade off, thanks for explaining. One one hand, it seems simpler if everything is embedded in the match and then we just verify that all of the chains have similar match ordering. On the other hand, there are downsides as you point out. I'm fine either way, but if we keep it like this can we beef up the docs? It's a bit confusing IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the docs are clearly lacking, and it's really confusing. I think we need to give proper examples and start from use cases. Let me try to get some istio examples, may be that will help clarifying the right API.

// specific as defined by the predicate extension.
//
// In case, there is more than one filter chain candidates remaining after
// the process completes, the first of the filter chains in the order of
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there cases where this behavior is desirable? This has caused endless pain for Istio and other Envoy users I have talked to. See #12572 for a lot of discussion.

Perhaps this is an opportunity to allow opting into a behavior that makes the matching more similar to other matching systems and that is easier to use without having a bunch of duplicated filter chains to appease the matching system?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@howardjohn The structured match is important for nested trie-matching algorithm, I think we cannot drop that at this point. The specific issue you had is with the defaulting. Each extension should default to allow-all, not deny-all, like in the above example issue you linked. We are fixing that with enforcement that empty matching predicate matches everything. The problem then becomes that multiple match chains will overlap, so we are also addressing that with the declared order.

Copy link
Contributor

Choose a reason for hiding this comment

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

Its a lot more than default, although that is how it started.

I am not sure the trie algorithm is broken, it just may be slower since you do not filter everything out at each step.

For example, if I want:

FC 1 matches on some complex criteria, say destination_port=80,transport_protocol=tls,application_protocols=[h2]
FC 2 matches everything else

If I want to do this, instead of a single FC2, I need to make every possible permutation of FCMs. So I need 65535 (destination ports) * 2 (transport_protocols) * infinity (all permutations of ALPN, which is unbounded.

I know we discussed a port range and we have a default FCM now, but the general problem still persists. If you look at Istio config today we duplicate a bunch of filter simply to appease this rule. The root cause is that any new predicate you add, you then need to take all existing filter chains and duplicate them to have a match + not a match in most cases.

Just consider routing for example... if I have a match for /foo and for Header:foo=bar, then later add a match for /foo && Header:foo=bar, a call to /foo without header foo=bar would not match if it used FCM logic. I don't think any user expects that adding an additional match makes less things match. Certainly control plane authors don't, as none of the Istio or TD control plane maintainers knew about this for a very long time.

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 will allow wildcards for ports, transport security, and ALPN, so the first issue is solved. We can also consider complement expression (not-expression), if necessary. Basically, there's nothing wrong with trie as long as the complement fits into the domain well. The idea is to extend the match conditions to avoid duplication of the chains, which makes sense.

Second issue is again similar. We'll just assume any value for path or header matches if not specified, unlike the existing proto. Yes, this was a mistake in the original definition, I think.

Is there something beyond defaulting and more expressive match conditions?

Copy link
Contributor

Choose a reason for hiding this comment

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

We'll just assume any value for path or header matches if not specified, unlike the existing proto.

I think this may be sufficient, will look into it a bit more

@kyessenov
Copy link
Contributor Author

kyessenov commented Nov 4, 2021

I think the crux of the problem is that the specificity elimination results in implicit negation of conditions from other filter chains.
For example, consider this example:

match1:
- SNI: *.com
- ports: 0-1000
match2:
- SNI: host.com
- ports: 80

And the following two inputs:

host.com, 81
x.com, 80

If we order SNI first, then the first input is rejected because match 2 eliminates match1 after SNI. If we order port first, then the second input is rejected because match2 eliminates match1 after ports. So it's impossible to express succinctly the desire to have a fallback filter chain. One has to define 4 matches with all combinations, and three duplicate filter chains.

My proposal is to add a boolean (call it continue) to each match condition indicating that specificity matching should not eliminate a particular filter chain:

match1:
- SNI: *.com, continue: true
- ports: 0-1000, continue: true

The algorithm then does not eliminate a wildcard match even if the more specific match exists. Logically, this accounts to duplicating the chain, and replacing the particular value with any other more specific value, which actually matches the user intent here. E.g. the above can be rolled out as:

match1:
- SNI: *.com
- ports: 0-1000
match1a:
- SNI: host.com (as well as other specific values matching the wildcard)
- ports: 0-1000
match1b:
- SNI: *.com (as well as other specific values matching the wildcard)
- ports: 80
match1c:
- SNI: host.com (as well as other specific values matching the wildcard)
- ports: 80

Thoughts about this proposal?

@lambdai
Copy link
Contributor

lambdai commented Nov 4, 2021

match1:

  • SNI: *.com, continue: true
  • ports: 0-1000, continue: true

The continue seems acting as a opt-in fallback. This is nice to have.

A couple of questions

  1. Combining your declared multiple chains are matching, the first is selected. How is the order defined?

  2. What's the computation complexity of selecting the filter chain?

@kyessenov
Copy link
Contributor Author

kyessenov commented Nov 4, 2021

  1. Combining your declared multiple chains are matching, the first is selected. How is the order defined?

For overlapping values, the choice is based on the order of the chains.

  1. What's the computation complexity of selecting the filter chain?

I think it's the same as if we had unrolled into 4 chains. Perhaps, we can do better in the implementation since we have more awareness of the common filter chains. Do you think there is a trie that can do better in this example?

@lambdai
Copy link
Contributor

lambdai commented Nov 4, 2021

  1. Combining your declared multiple chains are matching, the first is selected. How is the order defined?

For overlapping values, the choice is based on the order of the chains.

The natural order in the repeated filter_chains field? It may require the control plane to well ordered if the control plane wants a stable order while adding or remove unrelated filter chain. That's not ideal but I don't know how bad it is

Signed-off-by: Kuat Yessenov <kuat@google.com>
Signed-off-by: Kuat Yessenov <kuat@google.com>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks, flushing out a few more questions/comments.

/wait

message DestinationPort {
// Optional destination port to consider when use_original_dst is set on the
// listener in determining a filter chain match.
google.protobuf.UInt32Value destination_port = 1
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 have a range, do we need the single port? You can always put a single port into the range definition so there is no backwards compat issue?

[(validate.rules).uint32 = {lte: 65535 gte: 1}];

// Match destination port by range.
type.v3.Int32Range destination_port_range = 5;
Copy link
Member

Choose a reason for hiding this comment

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

Should this be a repeated set of ranges?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, makes sense.

// The criteria is satisfied if the source port of the downstream connection
// is contained in at least one of the specified ports. If the parameter is
// not specified, the source port is ignored.
repeated uint32 source_ports = 1
Copy link
Member

Choose a reason for hiding this comment

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

Repeated set of ranges to make all of the port matching consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines 346 to 353
// For example, consider SNI ``www.example.com``and two filter chains with
// the predicates ```*.example.com``` and ```*.com```. If ``fallthrough``
// flag is not set then only the filter chain ```*.example.com``` matches. If
// ``fallthrough`` flag is set on ```*.com```, then both filter chains match.
// In general, the order of specificity is domain specific as defined by the
// predicate extension. The flag should be set when matching on multiple
// properties in order for a default chain to apply without explicit specific
// matching of the first properties in the list.
Copy link
Member

Choose a reason for hiding this comment

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

Can you beef this up even a bit more with a worked example? I'm having a hard time wrapping my head around the case in which someone would want multiple matches, and also, if there are multiple matches, what does that mean for trie/sequential based matching? Does it basically mean that if there are multiple matches we keep recursing downward on N tree branches to see if there are further reductions in the search space?

Copy link
Contributor

Choose a reason for hiding this comment

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

Related to trie/sequential-based matching, would it make sense for this to use the new generic matching API that @snowp has been working on, rather than inventing yet another matcher structure?

https://github.com/cncf/xds/blob/main/xds/type/matcher/v3/matcher.proto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mattklein123 Adding more explanations. The extra matches are already inevitable in the existing API because of overlapping wildcards. What we're trying to change is making it more succinct for the control plane to express. I think there is normally no need to have multiple matches at the very end, but in the middle of matching, the "default" case has to be kept around in the search space until the full "special" case is matched. What happens now, is that control plane has to provide many identical "default" cases for each step of "special" case matching, and that quickly proliferates.

I think we can implement the fallthrough wildcard internally with trie upward propagation. E.g. if there is a chain FC1 wildcard "*.com" and trie node "example.com" for FC2, then we automatically add FC1 to node "example.com". This what control plane would have to do explicitly.

@markdroth I took a look at the generic matcher API. I think it's rather difficult to use. Our main issue with the existing state is that it's too hard to use right (e.g. Istio will not migrate to the status quo API and will stay on the "undeprecated" field). Make it more abstract does not seem to help with usability IMHO. It also seems L7-oriented right now, and the set of matchers is rather distinct. I can be convinced the other way, if we can construct some examples that are easy to comprehend.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the semantics of the generic matching API are actually much clearer and easier to understand, because instead of memorizing a set of precedence rules, you can explicitly encode the precedence in the matcher structure. It's actually not abstract at all; it's very explicitly and precise about what it represents.

The generic matching API was specifically designed to be extensible, so that you can plug in whatever inputs and whatever new match types you need. It was intentionally designed not to be L7-specific. For background, see https://docs.google.com/document/d/1G4g-6q0IArz_ERgqixzZCM0-wbexFYuBUvT9hkV7QRU/edit.

I really don't think we should be reinventing this wheel yet again. In the long run, we want all matching in xDS to move to the new generic matching API.

Copy link
Member

Choose a reason for hiding this comment

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

It hadn't occurred to me to use the generic matching API for this, but given that it inherently supports sub-linear matching, I tend to agree with @markdroth that we should see if we can use it. Can you mock up what that might look like and we can discuss? If there are usability issues with that API we should fix them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, I understand that any trie decision tree could be encoded in abstract sense. But I'm not seeing the succinct representation that avoids repeated (2^n) filter chains with slightly varied matching conditions.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that use of an API extension for an input or a matcher does not imply that the functionality is not a first-class citizen; it simply implies that it's a protocol-specific input or match type that is not built into the generic matching framework itself. For example, HTTP header inputs are an extension, and that is very much a first-class citizen in the API.

The idea of the generic matching API is that you construct a tree of matchers to represent things like AND and OR operations, where each individual node in the tree can be either a MatcherList (for linear matching) or a MatcherTree (for sublinear matching). In either case, if a match is found, we use the corresponding OnMatch, which can be either a protocol-specific action or a nested matcher (i.e., another node in the tree of matchers). If no match is found in a node, there is another OnMatch called on_no_match that will be used if populated; if it's not populated, then the node is considered not to match, and matching resumes from there.

For the example you cited, you'd structure it as the top node being a MatcherList, where the matchers in the list are:

  1. Match on path=/path. If that matches, use a nested matcher that checks for the header X = Y. (If the nested matcher does not match and does not have an on_no_match field, then we will move on to the next entry in the list.)
  2. Match on path prefix /.

Alternatively, if you wanted that second rule to be applied to anything that was not matched previously (i.e., you wanted it to apply to all requests, not just those with prefix="/"), you could move it out of the list and put it in the on_no_match field in the top node.

Copy link
Contributor Author

@kyessenov kyessenov Nov 11, 2021

Choose a reason for hiding this comment

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

Is there a way to build a matcher tree internally in Envoy? I think we have to use an outer MatcherList because of the defaulting behavior. But then each item might have overlapping predicates. For example,

  1. MatchList item1: Match on destination port 80.
  2. MatchList item2-1000: Match on other ports in the range of 1-10000.
  3. MatchList item: Match on destination port range 80-10000.

The linear semantics is sub-optimal here at the outer level. We want an implicit lookup tree, but we also don't want directly go into individual node 1-1000 without checking the default case 1001, ideally without iteration over items.

Copy link
Member

Choose a reason for hiding this comment

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

Is there a way to build a matcher tree internally in Envoy

You should be able to build up any config internally that you like, though if there are API issues with the type of matching you want to do we should sort that out.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know why we'd want to build the tree internally. I think the goal here should be to allow the API to explicitly configure how the matching should be done, so that there's no more counter-intuitive magic here.

I can tell you from when we implemented this logic in gRPC recently that the current matching behavior here is very difficult to understand. The behavior is that it basically hard-codes the order of matching to the following list:

  1. destination port
  2. destination IP
  3. server name
  4. transport protocol
  5. application protocol
  6. connection source type
  7. source IP
  8. source port

Trying to reason through how a given connection will be matched to a filter chain requires that you memorize that list, and even then, it takes real thought and it's very confusing to implement correctly. I think making this explicit in the API would be a big improvement.

For the destination port example, I think it should be fairly simply to define a new type of sublinear matcher that is keyed on port ranges. You could have one entry for ports 1-79, one for port 80, and one for ports 81-10000. Any given incoming connection would fit into exactly one of those three categories, so there's no need for linear matching behavior.

The only complication I see here is that there will wind up being multiple leaves in the tree for each filter chain, and we don't want to have to duplicate the filter chain for each leaf that uses it. But I think this can be addressed fairly simply by moving the actual filter chain definitions to a separate map, keyed by some opaque name, and then having the matcher leaves refer to the keys in that map. That way, whenever multiple leaves refer to the same chain, the only duplication is the opaque name.

@@ -109,6 +109,131 @@ message FilterChainMatch {
EXTERNAL = 2;
}

// Matches filter chains by the destination port.
// [#next-free-field: 6]
message DestinationPort {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the two fields in this proto be in a oneof? Or are there cases where they will both be used at the same time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's technically OR operator, so it's more expressive without oneof, I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay. Please make that clear in the comments.

Comment on lines 346 to 353
// For example, consider SNI ``www.example.com``and two filter chains with
// the predicates ```*.example.com``` and ```*.com```. If ``fallthrough``
// flag is not set then only the filter chain ```*.example.com``` matches. If
// ``fallthrough`` flag is set on ```*.com```, then both filter chains match.
// In general, the order of specificity is domain specific as defined by the
// predicate extension. The flag should be set when matching on multiple
// properties in order for a default chain to apply without explicit specific
// matching of the first properties in the list.
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to trie/sequential-based matching, would it make sense for this to use the new generic matching API that @snowp has been working on, rather than inventing yet another matcher structure?

https://github.com/cncf/xds/blob/main/xds/type/matcher/v3/matcher.proto

Signed-off-by: Kuat Yessenov <kuat@google.com>
@kyessenov
Copy link
Contributor Author

kyessenov commented Nov 11, 2021 via email

@markdroth
Copy link
Contributor

I disagree about having to partition the search space explicitly at every leaf. That does not reflect the reality of it's application in service mesh. The problem is that decisions depend on a variety of factors. So the going up one level in the tree to pick the next default is mandatory. In the example I mentioned, imagine there is a submatcher for every matcher that checks, for example, ALPN. If the submatcher fails we do want to backtrack to the port selection. What the matcher API misses is ability to quickly backtrack to the next best wildcard instead of iterating over a list.

I don't see any reason that the generic matcher API can't do this. It should be possible to write a sublinear matcher whose behavior is to fall back to the next-best match when the best match fails somewhere further down the tree.

I was actually just discussing this with @snowp earlier today. It turns out that the sublinear prefix matcher is actually not yet implemented, so the only sublinear matcher we have today is exact-match, for which there's no need to fall back to the next-best match. But I think that when we do implement the sublinear prefix matcher, we will probably want to fall back to the next-best match, so that MatcherTree effectively works the same way as MatcherList. (In other words, the fact that it uses a sublinear-time algorithm should not affect the logical semantics.) And I think you could do the same thing with (e.g.) an ALPN matcher.

@kyessenov
Copy link
Contributor Author

OK, so I think we're on the same page about the desired semantics. I think the fallback should not be required, it's reasonable to never match wildcard if there is a more specific match. That's how current implementation works in listeners, so there's prior art.

How do we want to do the listener filter chain refactor then? My original motivation was just to add metadata matching to listeners because it's blocking extensibility there, and it has grown way out of scope to cover changes to xDS matchers. Do we need to hold on adding metadata matching to listeners until xDS matchers are improved, and how long would that take to make them default in listeners?

@mattklein123
Copy link
Member

How do we want to do the listener filter chain refactor then? My original motivation was just to add metadata matching to listeners because it's blocking extensibility there, and it has grown way out of scope to cover changes to xDS matchers. Do we need to hold on adding metadata matching to listeners until xDS matchers are improved, and how long would that take to make them default in listeners?

The problem is we keep accruing technical debt wrt filter chain matching and I would really rather not add yet more debt. How much work is it really to do it "the right way?" What changes are actually needed to the generic matching infra to support this? cc @snowp

@markdroth
Copy link
Contributor

Matt, thanks for your insight here. I think that when I made my comments earlier, I was actually confused between selecting a listener and selecting a filter chain within a listener. I now realize that in this PR we are talking only about the latter, which does make things simpler.

My understanding of the current behavior is that if anything in a single listener changes (including filter chain matchers), Envoy currently drains all connections for that listener. I agree that we can continue to do that -- i.e., go with your option (2) -- and that that should basically preserve the existing behavior.

Thanks, and sorry for the confusion.

@kyessenov
Copy link
Contributor Author

@markdroth Listener filter chain is "intelligent" in the sense that if a filter chain is added, then existing connections are not drained. Note that the filter chain includes the condition as well, which can take priority over conditions for existing connections. So what happens now is actually none of the options 1-3.

This is valuable for "eventual service discovery" systems. Services may come up gradually, and resetting all connections every time an edge event happens causes too much churn. I think it's a strong requirement to preserve connections when new chains are introduced but because of the shared matching tree, this condition is difficult to express precisely.

@markdroth
Copy link
Contributor

Ah, okay, I didn't realize that the current behavior was more nuanced. In that case, the proposed option (2) would actually be a regression.

If we have a strong requirement to avoid disrupting existing connections unnecessarily, then I don't see any option here other than to save the info about each connection that we need to reevaluate the match later when we get a config update.

@mattklein123
Copy link
Member

If we have a strong requirement to avoid disrupting existing connections unnecessarily, then I don't see any option here other than to save the info about each connection that we need to reevaluate the match later when we get a config update.

I actually think option 1 (do nothing) is a reasonable thing to do in the first version. I don't see a problem with the matches being eventually consistent with regard to which connections are matched to which chain.

Signed-off-by: Kuat Yessenov <kuat@google.com>
@kyessenov
Copy link
Contributor Author

Updated to follow choice (1): do not re-evaluate and maintain connections when conditions change.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks this LGTM modulo figuring out some docs stuff. I'm fine if you want to move to code at this point and we can review it all as a complete PR? Thanks a ton for working on this. This will be a great improvement.

/wait

// * otherwise, if the destination port is 443, then the filter chain "https" is selected;
// * otherwise, the default filter chain is selected (or the connection is rejected without the default filter chain).
//
// .. code-block:: yaml
Copy link
Member

Choose a reason for hiding this comment

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

Can we make this stanza use the actual config checking/validation version so it doesn't get out of date?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ack, pending relevant protos merged.

// filter chain is removed or structurally modified, then the drain for its
// connections is initiated.
//
// [#not-implemented-hide:]
Copy link
Member

Choose a reason for hiding this comment

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

I see this configuration as being one of the ones that will be very difficult for users to actually piece together without examples. Can we make sure that somehow we wire up the extension docs system here so that this somehow lists out all supported match inputs and relevant typed configs, etc.? We need to guide users much more specifically on how to use this new field.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, we need more examples. I can add some but I'd really need #19493 merged first for protos to become available. There is an issue with cncf/xds protos not being documented.

snowp pushed a commit that referenced this pull request Feb 17, 2022
Introduce data inputs for connection matching as part of #18871

Signed-off-by: Kuat Yessenov <kuat@google.com>
Signed-off-by: Kuat Yessenov <kuat@google.com>
@mattklein123
Copy link
Member

@kyessenov what's the status of this. Is this ready for review?

/wait-any

Signed-off-by: Kuat Yessenov <kuat@google.com>
@kyessenov
Copy link
Contributor Author

Thanks for taking a look. My update is that I started implementation, but pretty early in it. Do you think we can get this merged since the APIs are hidden for now? This is a rather long discussion to track. I verified locally that the hidden example in docs works.

@mattklein123
Copy link
Member

@kyessenov at this point this PR is pretty small. Do you want to just close it and then just reopen with the implementation? Then we can review it all together? In general this LGTM.

@kyessenov kyessenov closed this Feb 24, 2022
snowp pushed a commit that referenced this pull request Apr 12, 2022
Add unified matcher for network streams, as a replacement for filter chain match. 

See previous discussion in #18871

Signed-off-by: Kuat Yessenov <kuat@google.com>
vehre-x41 pushed a commit to vehre-x41/envoy that referenced this pull request Apr 19, 2022
Add unified matcher for network streams, as a replacement for filter chain match. 

See previous discussion in envoyproxy#18871

Signed-off-by: Kuat Yessenov <kuat@google.com>
Signed-off-by: Andre Vehreschild <vehre@x41-dsec.de>
ravenblackx pushed a commit to ravenblackx/envoy that referenced this pull request Jun 8, 2022
Add unified matcher for network streams, as a replacement for filter chain match. 

See previous discussion in envoyproxy#18871

Signed-off-by: Kuat Yessenov <kuat@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for configurable precedence of FilterChainMatch rules.