Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agents/ARCHITECTURE-GATEWAY.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ per-VPC gateway design to colocate with the workload's own VRF.
| CRD | Scope | Written by | Purpose |
| ---------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NetworkGateway` | Namespaced, one per gateway node (`spec.targetRef.name` = node name) | Operator, once per gateway node (see [worked example](#worked-containerlab-example) below) | Node-scoped root object, mirroring `BGPRouter`'s pattern. Identifies which nodes participate in the anycast mesh and surfaces each node's engine health via `status.conditions`. Carries **no** self-address field — DSR rewrites nothing, so there is no SNAT source to publish. |
| `NetworkRule` | Namespaced, tenant-writable | Tenant, via an admission webhook that verifies VPC/VPCAttachment ownership (**webhook not yet deployed in this repo** — see Known Constraints) | Ingress load-balancing spec: `vpcRef`/`vpcAttachmentRef` (opaque tenant identifiers), `vipAddresses` (1–8, IPv4/IPv6), `protocol` (`tcp`/`udp`), `port`, `backends` (1–64 `address:port` pairs). Served by every `NetworkGateway` in the namespace identically — there is **no** `status.primaryNode` field. |
| `NetworkRule` | Namespaced, tenant-writable | Tenant, via an admission webhook that verifies VPC/VPCAttachment ownership (**webhook not yet deployed in this repo** — see Known Constraints) | Ingress load-balancing spec: `vpcRef`/`vpcAttachmentRef` (opaque tenant identifiers), `vipAddresses` (1–8, IPv4/IPv6), `protocol` (`tcp`/`udp`), `port`, `backends` (1–64 `address:port` pairs; each backend `port` must equal the rule `port` — DSR cannot remap ports). Served by every `NetworkGateway` in the namespace identically — there is **no** `status.primaryNode` field. |

Both are defined in `go.datum.net/network`'s `api/v1alpha1` package
(`gateway_types.go`, `rule_types.go`) — the same external CRD module the
Expand Down
20 changes: 20 additions & 0 deletions internal/webhook/networkrule_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (v *NetworkRuleValidator) ValidateDelete(
// object has passed admission and been persisted: a validating webhook cannot
// write to the status of a request it has not yet admitted.
func (v *NetworkRuleValidator) authorize(ctx context.Context, rule *networkv1alpha1.NetworkRule) error {
if err := validateBackendPorts(rule); err != nil {
return err
}

req, err := admission.RequestFromContext(ctx)
if err != nil {
return fmt.Errorf("resolve admission request: %w", err)
Expand Down Expand Up @@ -93,3 +97,19 @@ func (v *NetworkRuleValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {

// Production registration additionally requires a webhook configuration and
// service manifest, plus TLS provisioning, neither of which is wired up here.

// validateBackendPorts refuses a NetworkRule whose backends declare a port other
// than spec.port. The gateway datapath never rewrites ports (Direct Server
// Return); a mismatched backend port is accepted today and then ignored, so the
// backend never sees traffic on the port it was configured with (#737).
func validateBackendPorts(rule *networkv1alpha1.NetworkRule) error {
for i, b := range rule.Spec.Backends {
if b.Port != rule.Spec.Port {
return fmt.Errorf(
"backends[%d].port (%d) must match spec.port (%d): Direct Server Return cannot remap ports",
i, b.Port, rule.Spec.Port,
)
}
}
return nil
}
39 changes: 39 additions & 0 deletions internal/webhook/networkrule_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,42 @@ func TestAllowAllAuthorizer_AlwaysAllows(t *testing.T) {
t.Fatal("AllowAllAuthorizer must always allow")
}
}

func TestNetworkRuleValidator_BackendPortMustMatchRulePort(t *testing.T) {
v := &NetworkRuleValidator{Authorizer: fakeAuthorizer{allow: true}}

mismatch := testRule()
mismatch.Spec.Port = 443
mismatch.Spec.Backends = []networkv1alpha1.NetworkRuleBackend{
{Address: "10.0.0.1", Port: 8443},
}
_, err := v.ValidateCreate(contextWithRequester("alice"), mismatch)
if err == nil {
t.Fatal("expected refusal when backend port differs from spec.port")
}
if want := "Direct Server Return cannot remap ports"; !containsStr(err.Error(), want) {
t.Errorf("error %q does not mention %q", err.Error(), want)
}

match := testRule()
match.Spec.Port = 443
match.Spec.Backends = []networkv1alpha1.NetworkRuleBackend{
{Address: "10.0.0.1", Port: 443},
}
if _, err := v.ValidateCreate(contextWithRequester("alice"), match); err != nil {
t.Fatalf("matching ports should be admitted: %v", err)
}
}

func containsStr(s, sub string) bool {
return len(sub) == 0 || (len(s) >= len(sub) && (s == sub || len(s) > 0 && containsAt(s, sub)))
}

func containsAt(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}