Skip to content

refactor(networkpolicy): consolidate egress destination parsing - #5223

Open
tianfeng92 wants to merge 2 commits into
tigera:masterfrom
tianfeng92:EV-6963-consolidate-egress-parsing
Open

refactor(networkpolicy): consolidate egress destination parsing#5223
tianfeng92 wants to merge 2 commits into
tigera:masterfrom
tianfeng92:EV-6963-consolidate-egress-parsing

Conversation

@tianfeng92

@tianfeng92 tianfeng92 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

ParseExternalDestination and ExternalDestinationEntityRule already turn an endpoint into the tightest egress rule available. Only the OTel Collector used them. Dex and Guardian carried their own copies; both now go through the shared pair.

Dex's parseHostPortFromURL was a duplicate of pkg/url.ParseHostPortFromHTTPProxyURL, which Guardian already used, so it is deleted rather than reimplemented.

Preserved: Dex's 0.0.0.0/0 and ::/0 any-destination rules, and its unconditional domain rule — it has no license input.

Changed: a <svc>.<ns>.svc destination now renders a Services match instead of a Domains rule, so it follows the Service's own ports. A Domains rule never worked for a ClusterIP. Only the domain branch needs egress-access-control; a Services match does not.

Testing

  • lint clean; render, enterprise, clusterconnection and authentication suites pass
  • new test covers an unlicensed in-cluster destination
  • draft until verified on a cluster: Dex reaching its IdP, Guardian's tunnel, with and without a proxy

Release Note

None

EV-6963

Dex, Guardian and the OTel Collector each turned an endpoint into an egress
rule. ParseExternalDestination and ExternalDestinationEntityRule already do
that, and only the collector used them.

Dex loses parseHostPortFromURL, a duplicate of pkg/url's
ParseHostPortFromHTTPProxyURL that Guardian already used, and builds its
specific-destination rules through the shared pair. Guardian does the same for
the tunnel destination. Its copy had moved to pkg/enterprise/clusterconnection
since the original review.

Two behaviours are kept rather than folded into the helper's defaults. Guardian
skips a named destination when the EgressAccessControl feature is absent
instead of falling back to port-only: the trailing Pass rule already governs
the tunnel, and a port-only allow would be wider than what ships today. Dex
renders the domain rule unconditionally because it has no license input, and
narrowing it would change IdP egress.

Dex's 0.0.0.0/0 and ::/0 any-destination rules are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review catch. The consolidation gated the whole rule on
EgressAccessControl whenever the destination was not a literal IP, but
ExternalDestinationEntityRule has three branches and only the domain one needs
the feature. An in-cluster management address renders a service match, which
does not, so an unlicensed cluster lost the rule for its own tunnel.

Ask for the rule with the feature we actually have and skip it only when that
left nothing to match on. Covered by a test that fails against the previous
gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tianfeng92 tianfeng92 changed the title [Operator][Dex][Guardian] Consolidate egress destination parsing refactor(networkpolicy): consolidate egress destination parsing Aug 19, 2026
@tianfeng92
tianfeng92 marked this pull request as ready for review August 19, 2026 18:49
@tianfeng92
tianfeng92 requested a review from a team as a code owner August 19, 2026 18:49
Copilot AI lite review requested due to automatic review settings August 19, 2026 18:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors egress-destination parsing and rule construction to use shared helpers, removing duplicate logic across components while preserving existing licensing and behavior constraints for Dex and Guardian.

Changes:

  • Dex: replaces local host/port extraction and destination parsing with pkg/url.ParseHostPortFromHTTPProxyURL and networkpolicy.ParseExternalDestination + ExternalDestinationEntityRule.
  • Guardian: replaces inline tunnel-destination rule building with the shared parsing/rule helpers while preserving the “skip port-only allow when unlicensed” behavior.
  • Tests: adds coverage ensuring an in-cluster <svc>.<ns>.svc tunnel destination renders as a Service match even without the license feature.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
pkg/render/dex.go Removes duplicated URL/egress destination parsing and uses shared helpers for consistent rule rendering.
pkg/enterprise/clusterconnection/guardian.go Consolidates tunnel destination rule generation via shared parsing/entity-rule helpers while preserving licensing behavior.
pkg/enterprise/clusterconnection/guardian_render_test.go Adds a regression test for Service-match rendering of in-cluster tunnel destinations without the license feature.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@pasanw pasanw Aug 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A large portion of my feedback is about the ParseExternalDestination implementation, which I should have given to you in the previous PR - apologies that I missed that.

I'll give that feedback here:

  • clusterService logic doesn't look quite right - if it's passed something like proxy.corp.svc.example.com it will treat it as a kubernetes service. I think this function should be strict and only parse service name and namespace if the host string ends with .svc, .svc.<cluster-domain>, or .svc.<cluster-domain>.. This scheme doesn't support if a customer specifies their service without a .svc based suffix, but I couldn't think of a clean way to support that - we can brainstorm on that if you'd like
  • ExternalDestinationEntityRule results in a fail-open style of policy, where if we aren't able to restrict the host in the egress rule (with a Domains or Nets field), we return an egress rule that allows all hosts. I don't think a general policy helper should be making a decision to open broad access by default - that kind of decision should be made by the component. (And I don't think components should be choosing broad access in such a scenario, including otel collector)
  • Guardian, Dex, and other components all utilized the flow of SplitHostPort > PortFromString > ParseIP. Looks like we replaced PortFromString with Atoi - could we revert that? Atoi doesn't support all of the different scenarios that PortFromString does and in general I think it's better to not change longstanding logic
  • I think it would be nicer for components to provide their destination string to one function and get back an EntityRule. This way it's only one function call and no new concept (ExternalDestination) needs to be introduced. I'd recommend structuring it as such:
    • func X(destination string) v3.EntityRule, which itself calls func Y(destination string) (host string, port string) and then func Z(host string, port string) v3.EntityRule to do its work
    • OTel, Guardian, Dex would use X directly to get their entity rule
    • Manager and ServiceEndpoint would use Z to get their entity rule

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

General comment on this PR: looks like we are missing the opportunity for manager.go (managerComponent) and k8s-endpoint.go (ServiceEndpoint) to utilize our shared logic. They could use the function Z, as mentioned above

// the Pass rule below already governs the tunnel, and a port-only allow
// here would be wider than what ships today.
dest := networkpolicy.ExternalDestinationEntityRule(parsed, gpc.IncludeEgressNetworkPolicy)
if dest.Services == nil && len(dest.Nets) == 0 && len(dest.Domains) == 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like this is a large guard trying to detect if the fail-open I mentioned in the larger comment occurred? If so, I would hope that we could remove it as a result of removing the fail-open behaviour

host, port, err := net.SplitHostPort(tunnelDestinationHostPort)
if err != nil {
return v3.NetworkPolicySpec{}, err
parsed, ok := networkpolicy.ParseExternalDestination(tunnelDestinationHostPort)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude: This replaced net.SplitHostPort, which rejected anything that was not host:port. ParseExternalDestination's url.Parse fallback now silently accepts a URL-shaped gpc.URL — e.g. https://mgmt.example.com parses as host mgmt.example.com port 443 (the scheme default), not the tunnel port. So a misconfig that used to fail the render loudly now ships a rule for the wrong port. Note the // gpc.URL has host:port form comment just above is no longer enforced. Since — unlike OTel's endpoint — this field is not meant to be a URL, Guardian should keep rejecting non-host:port here (a strict entry point, or validate before parsing).

Comment thread pkg/render/dex.go
}

httpProxyDestination, err := parseHostPortFromURL(httpProxyURL)
httpProxyDestination, err := operatorurl.ParseHostPortFromHTTPProxyURL(httpProxyURL)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude: Robustness note — pre-existing, not introduced here, low priority. Swapping to ParseHostPortFromHTTPProxyURL (here and at line 579) widens the set of proxy values that error out: it rejects any non-http/https scheme up front, whereas the old parseHostPortFromURL accepted anything that carried a port. The problem is what an error here costs: resolveEgressDestinationsForPod returns the error before the 0.0.0.0/0/::/0 catch-alls are appended, and the caller (resolveEgressRulesByDestination) then continues the whole pod — so a single unparseable proxy value silently drops that pod's entire egress, IdP catch-alls included (log line only, status stays healthy). The ordering and the whole-pod skip both predate this PR, and the newly-rejected schemes (e.g. socks5://, scheme-relative //host:port) aren't a supported proxy type anyway (Guardian's tunnel dialer is HTTP-CONNECT-only), so this is hardening, not a regression. If we want to harden it: append the catch-alls unconditionally, and handle a proxy-parse failure locally (skip just that one destination) so a bad proxy value can't wipe the broad allows.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants