{"record":{"id":"2f4cfcad57e621ee","repo":"grpc/grpc-go","slug":"multiple-filter-chains-with-overlapping-matching-r","errorCode":null,"errorMessage":"multiple filter chains with overlapping matching rules are defined","messagePattern":"multiple filter chains with overlapping matching rules are defined","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/xds/xdsclient/xdsresource/unmarshal_lds.go","lineNumber":598,"sourceCode":"\n\t// Not found, create a new entry.\n\tsrcPrefixes.Entries = append(srcPrefixes.Entries, SourcePrefixEntry{\n\t\tPrefix:  prefix,\n\t\tPortMap: make(map[int]NetworkFilterChainConfig),\n\t})\n\treturn addFilterChainsForSourcePorts(&srcPrefixes.Entries[len(srcPrefixes.Entries)-1], fc)\n}\n\nfunc addFilterChainsForSourcePorts(entry *SourcePrefixEntry, fc *v3listenerpb.FilterChain) error {\n\tports := fc.GetFilterChainMatch().GetSourcePorts()\n\tsrcPorts := make([]int, 0, len(ports))\n\tfor _, port := range ports {\n\t\tsrcPorts = append(srcPorts, int(port))\n\t}\n\n\tif len(srcPorts) == 0 {\n\t\tif !entry.PortMap[0].IsEmpty() {\n\t\t\treturn errors.New(\"multiple filter chains with overlapping matching rules are defined\")\n\t\t}\n\t\tfcc, err := filterChainFromProto(fc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tentry.PortMap[0] = fcc\n\t\treturn nil\n\t}\n\tfor _, port := range srcPorts {\n\t\tif !entry.PortMap[port].IsEmpty() {\n\t\t\treturn errors.New(\"multiple filter chains with overlapping matching rules are defined\")\n\t\t}\n\t\tfcc, err := filterChainFromProto(fc)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tentry.PortMap[port] = fcc\n\t}","sourceCodeStart":580,"sourceCodeEnd":616,"githubUrl":"https://github.com/grpc/grpc-go/blob/0c51461d27177d997e14c642fe18c11668fc09a3/internal/xds/xdsclient/xdsresource/unmarshal_lds.go#L580-L616","documentation":"Returned by addFilterChainsForSourcePorts when a new filter chain has no source_ports specified (so it would match any source port via the wildcard slot 0) but that wildcard slot is already occupied by a previously-seen filter chain with the same destination prefix, server name, source prefix, and no source ports. grpc-go cannot non-deterministically pick between two equally-matching chains, so it rejects the LDS resource.","triggerScenarios":"An LDS Listener defines two (or more) server filter chains whose filter_chain_match criteria collapse to the same key (same destination prefix, same source prefix/CIDR, same server name, neither specifying source_ports). Both would match the same inbound connection.","commonSituations":"Control plane generates multiple filter chains for different SNI/ports but two of them end up with identical match criteria after normalization. Duplicated/mirrored filter chain templates. Migration artifact where a new chain was added without removing the old default one.","solutions":["Disambiguate the filter chains: give each a distinct match criterion (e.g. distinct server_names, distinct source_ports, distinct destination prefix, or distinct source prefix).","If one chain is intended as the fallback, mark it as the default_filter_chain on the Listener instead of a separate FilterChain.","Remove the duplicate filter chain entry from the LDS resource.","Audit the control-plane translation logic that produced overlapping chain matches."],"exampleFix":"// before: two chains both match all ports\n//   filter_chains: [ { filter_chain_match: { source_prefix_ranges: [{address_prefix:\"10.0.0.0\",prefix_len:8}] }, ... },\n//                    { filter_chain_match: { source_prefix_ranges: [{address_prefix:\"10.0.0.0\",prefix_len:8}] }, ... } ]\n// after: differentiate via source_ports or use default_filter_chain\n//   filter_chains: [ { filter_chain_match: { source_prefix_ranges: [...], source_ports: [443] }, ... } ],\n//   default_filter_chain: { ... }","handlingStrategy":"validation","validationCode":"// Detect filter-chain match overlaps (wildcard-port case) before sending LDS to grpc-go.\nfunc detectChainOverlap(chains []*envoy_listener_pb.FilterChain) error {\n    type key struct{ dst, src, sni string }\n    seen := map[key]bool{}\n    for _, c := range chains {\n        m := c.GetFilterChainMatch()\n        if len(m.GetSourcePorts()) > 0 { continue }\n        k := key{dst: cidrsToString(m.GetPrefixRanges()), src: cidrsToString(m.GetSourcePrefixRanges()), sni: strings.Join(m.GetServerNames(), \",\")}\n        if seen[k] { return fmt.Errorf(\"two chains collapse to the same match: %+v\", k) }\n        seen[k] = true\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"Catch in LDS watcher; the resource is rejected until the control plane disambiguates. Log the listener name so the control-plane team can locate the duplicate.","preventionTips":["Use default_filter_chain for the catch-all instead of adding a wildcard FilterChain.","Write a policy check that computes the normalized match key per chain and rejects duplicates.","When adding a new chain, run the overlap check in CI before publishing the resource."],"tags":["xds","lds","filter-chain","ambiguous-match","control-plane"],"backgroundTag":null,"analyzedSha":"0c51461d27177d997e14c642fe18c11668fc09a3","analyzedAt":"2026-08-11T14:49:15.055Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}