Skip to content

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

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

refactor(networkpolicy): consolidate egress destination parsing#5223
tianfeng92 wants to merge 4 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

@marvin-tigera marvin-tigera added this to the v1.45.0 milestone Aug 19, 2026
@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.

Comment thread pkg/enterprise/clusterconnection/guardian.go
Comment thread pkg/enterprise/clusterconnection/guardian.go
Comment thread pkg/enterprise/clusterconnection/guardian.go Outdated
Comment thread pkg/enterprise/clusterconnection/guardian.go Outdated
Comment thread pkg/render/dex.go Outdated
tianfeng92 and others added 2 commits August 21, 2026 10:51
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>
Copilot AI review requested due to automatic review settings August 21, 2026 17:55
@tianfeng92
tianfeng92 force-pushed the EV-6963-consolidate-egress-parsing branch from 58fe5e5 to 458b68c Compare August 21, 2026 17:55

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

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

Comment thread pkg/render/common/networkpolicy/networkpolicy.go
Comment thread pkg/render/otelcollector/component.go
Comment thread pkg/enterprise/clusterconnection/guardian.go Outdated
Restructures the helper the way the review asked: EntityRuleForDestination
parses with ParseHostPort and builds with EntityRuleForHostPort, so a component
holding a host:port string makes one call and the ExternalDestination type is
gone. manager.go and k8s-endpoint.go now build their rules the same way.

The helper no longer fails open. It always constrains the destination, and a
component that cannot use the rule decides what to do rather than inheriting a
ports-only allow: Guardian drops a domain rule it has no licence for, which
also retires the guard that existed to detect the fail-open.

Service matching is strict. The host must be <service>.<namespace>.svc, with
the cluster domain and a trailing dot optional, so an external name that merely
contains an svc label is left as a domain. That needs the cluster domain to
disambiguate, which is why it is threaded through; Guardian and the apiserver
endpoint pass none, since neither destination is in-cluster.

Port parsing is back on numorstring.PortFromString, and ParseHostPort refuses a
URL rather than quietly taking the scheme's default port -- Guardian's field is
documented as host:port, and a URL there was silently becoming 443.

A proxy value Dex cannot parse now costs only its own rule. It used to abandon
the whole pod before the catch-alls were appended, so one bad string removed
that pod's IdP egress entirely.

Adds the first tests for this package, covering the strict matching and the
absence of a ports-only rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 21, 2026 18:02
@tianfeng92
tianfeng92 force-pushed the EV-6963-consolidate-egress-parsing branch from 458b68c to 224963e Compare August 21, 2026 18:02

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

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

Suppressed comments (1)

pkg/render/common/networkpolicy/networkpolicy.go:353

  • ParseHostPort can successfully parse destinations like ":443" (net.SplitHostPort allows an empty host) and return an empty host. That can later produce an invalid EntityRule (e.g., Domains: [""]). Reject empty-host destinations explicitly so misconfigurations fail loudly.
func ParseHostPort(destination string) (string, numorstring.Port, error) {
	host, portStr, err := net.SplitHostPort(destination)
	if err != nil {
		return "", numorstring.Port{}, err
	}
	port, err := numorstring.PortFromString(portStr)
	if err != nil {
		return "", numorstring.Port{}, err
	}
	return host, port, nil

Copilot AI review requested due to automatic review settings August 21, 2026 18:20

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

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

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

pkg/render/otelcollector/component.go:853

  • exporterDestination claims to default the port from the URL scheme, but when the endpoint has no explicit port it always defaults to 443. If a stale CRD ever allows non-https endpoints, this will silently pick the wrong port (and the comment is inaccurate). Consider defaulting based on u.Scheme and erroring on unknown schemes so the caller can safely skip the rule.
	portStr := u.Port()
	if portStr == "" {
		// https is the only scheme the API accepts.
		portStr = "443"
	}

pkg/render/otelcollector/component.go:859

  • EntityRuleForHostPort always returns a Domains match for hostnames. The OpenTelemetry collector rendering no longer carries a license/feature flag (EgressAccessControl) to decide whether Domains rules are allowed, so an otel-collector-only license could cause the operator to render NetworkPolicy objects that are rejected/unsupported (previously this was handled via DomainEgressAllowed). Consider reintroducing a domain-egress feature gate (or dropping/errored rules when dest.Domains is set but the feature is inactive).
		return v3.EntityRule{}, err
	}
	return networkpolicy.EntityRuleForHostPort(host, clusterDomain, port), nil
}

pkg/render/common/networkpolicy/networkpolicy_test.go:112

  • ParseHostPort has unit tests for URL and bare-host inputs, but it doesn't currently cover the empty-host case (e.g. ":9449"), which net.SplitHostPort accepts. Adding a test will prevent regressions around accidentally producing empty-domain rules.
		It("rejects a bare host", func() {
			_, _, err := networkpolicy.ParseHostPort("mgmt.example.com")
			Expect(err).To(HaveOccurred())
		})

pkg/render/common/networkpolicy/networkpolicy.go:349

  • ParseHostPort currently accepts destinations like ":9449" (empty host) because net.SplitHostPort allows it. That can flow into EntityRuleForHostPort and produce a Domains rule containing an empty string, which is likely invalid and can cause confusing failures later. Reject empty hosts explicitly here.
	host, portStr, err := net.SplitHostPort(destination)
	if err != nil {
		return "", numorstring.Port{}, err
	}
	port, err := numorstring.PortFromString(portStr)

Copilot AI review requested due to automatic review settings August 21, 2026 18:36
@tianfeng92
tianfeng92 force-pushed the EV-6963-consolidate-egress-parsing branch from a1aa678 to 79f0b26 Compare August 21, 2026 18:36

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

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

Suppressed comments (2)

pkg/render/common/networkpolicy/networkpolicy.go:353

  • ParseHostPort accepts ":" (empty host) because net.SplitHostPort returns host=="" without error. That flows into EntityRuleForHostPort and produces Domains: [""] (or other invalid rules), which can later cause render/apply failures or unintended behavior. Reject empty hosts explicitly.
func ParseHostPort(destination string) (string, numorstring.Port, error) {
	host, portStr, err := net.SplitHostPort(destination)
	if err != nil {
		return "", numorstring.Port{}, err
	}
	port, err := numorstring.PortFromString(portStr)
	if err != nil {
		return "", numorstring.Port{}, err
	}
	return host, port, nil

pkg/render/otelcollector/component.go:875

  • This comment claims Validate rejects endpoints that exporterDestination cannot parse, but OpenTelemetrySpec.Validate currently only enforces an https:// prefix (api/v1/logcollector_types.go:344-349) and does not validate URL parsing or port correctness. That means malformed endpoints (e.g., bad port) will hit this path and be silently skipped, making exports fail without a clear configuration error. At minimum, update the comment to reflect reality; ideally tighten Validate (or fail rendering) so users get an actionable error instead of a silent skip.
		dest, err := exporterDestination(exp, c.cfg.ClusterDomain)
		if err != nil {
			// Validate rejects an endpoint this cannot parse, so reaching here means
			// a stale CRD let one through. Skip the rule rather than widening it:
			// the default-deny then blocks that exporter, which is the safe failure.
			continue
		}

…st callers

Service matching now requires a cluster domain. Passing none gives the plain
host-to-rule behaviour -- an exact net for a literal IP, otherwise the domain --
so manager.go and k8s-endpoint.go can share the helper without their rules
changing shape. A Services match cannot carry ports, and both of those callers
have ports they need to keep: the two standard LDAP ports, and the apiserver's.

Only the OTel collector opts in, which is where in-cluster exporters are
expected and where that behaviour already shipped.

Guardian's in-cluster test went with it: it asserted a service match that only
held while the branch was unconditional. Replaced with the property that does
hold -- a literal IP is pinned to an exact net whether or not the cluster is
licensed for domain egress.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 21, 2026 20:59
@tianfeng92
tianfeng92 force-pushed the EV-6963-consolidate-egress-parsing branch from 79f0b26 to d91a580 Compare August 21, 2026 20:59

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

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

pkg/render/otelcollector/component.go:868

  • When exporterDestination fails to parse an exporter endpoint, the error is silently ignored (continue) and no egress rule is rendered. That can leave exports blocked by default-deny with no signal in operator logs about why the destination was skipped.
		dest, err := exporterDestination(exp, c.cfg.ClusterDomain)
		if err != nil {
			continue
		}

Comment on lines +846 to 849
portStr := u.Port()
if portStr == "" {
portStr = "443"
}
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