Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
049ee94
proxy: scope datapath rules to the node hosting the backend
mattia-eleuteri Aug 7, 2026
8aee75f
proxy: do not abort the pod when startup cleanup hits ENOENT
mattia-eleuteri Aug 7, 2026
1605704
proxy: keep the egress SNAT map cluster-wide
mattia-eleuteri Sep 15, 2026
1110b24
proxy: retry cleanup deletions per element on ENOENT
mattia-eleuteri Sep 15, 2026
0cdd984
proxy: serialize datapath writes and isolate conflict removals
mattia-eleuteri Sep 15, 2026
76f2548
proxy: retry failed datapath writes, commit additions strictly
mattia-eleuteri Sep 15, 2026
2eafd9a
controllers: serialize whole reconciliations against the retry loop
mattia-eleuteri Sep 15, 2026
0b10c26
controllers: retry failed datapath withdrawals
mattia-eleuteri Sep 15, 2026
c78640c
controllers: build the startup snapshot from the informer stores
mattia-eleuteri Sep 15, 2026
494dbf7
controllers: requeue the startup cleanup when its retry fails
mattia-eleuteri Sep 15, 2026
9f5a546
chart: inject NODE_NAME into the DaemonSet
mattia-eleuteri Oct 1, 2026
4b264c4
docs: propose an L4 LoadBalancer mode
mattia-eleuteri Oct 2, 2026
6cdb2bf
l4: select the services the L4 mode takes over
mattia-eleuteri Oct 2, 2026
7377375
l4: compute what a node programs for the L4 mode
mattia-eleuteri Oct 2, 2026
0c3b666
l4: decide which conntrack entries a sync leaves stale
mattia-eleuteri Oct 2, 2026
e8a6e7b
proxy: program the L4 mode's table with nftables
mattia-eleuteri Oct 2, 2026
cb5bc01
proxy: purge conntrack entries over netlink
mattia-eleuteri Oct 2, 2026
98c6c95
proxy: send real traffic through the L4 table in tests
mattia-eleuteri Oct 2, 2026
a7237b6
controllers: reconcile the L4 mode from the informer caches
mattia-eleuteri Oct 2, 2026
6576eab
main: run the L4 mode behind --enable-l4-loadbalancer
mattia-eleuteri Oct 2, 2026
ccc5db0
chart: add an l4LoadBalancer.enabled switch
mattia-eleuteri Oct 2, 2026
49ce097
proxy: check that rebuilding the L4 table keeps connections alive
mattia-eleuteri Oct 2, 2026
d32b1c6
proxy: check the Close errors in the L4 tests
mattia-eleuteri Oct 2, 2026
c2f7158
docs: document the L4 mode and align the RFC with the prototype
mattia-eleuteri Oct 2, 2026
b52e84f
l4: report each notice once
mattia-eleuteri Oct 2, 2026
bed97a7
main: allow running the L4 mode without the VM mode
mattia-eleuteri Oct 2, 2026
fd5b853
l4: let not-ready and terminating backends drain
mattia-eleuteri Oct 2, 2026
dedc066
controllers: keep a forced L4 pass forced until it succeeds
mattia-eleuteri Oct 2, 2026
1ee6f3d
proxy: give the L4 sets IDs of their own transaction
mattia-eleuteri Oct 2, 2026
a83bd5b
l4: drain only terminating backends, purge not-ready ones
mattia-eleuteri Oct 2, 2026
181b41c
docs: reconcile the L4 mode with the Kyverno guard on CCM Services
mattia-eleuteri Oct 2, 2026
b003f67
proxy: rename the L4 dnat chain so nft can name it
mattia-eleuteri Oct 2, 2026
dc0eef2
controllers: log the L4 mode under its own name
mattia-eleuteri Oct 2, 2026
3d69216
docs: record the lab run of the L4 mode
mattia-eleuteri Oct 2, 2026
c1a0642
l4: purge TCP flows with their frontend only, as kube-proxy does
mattia-eleuteri Oct 2, 2026
fbeca1a
docs: correct what the Kyverno guard refuses on CCM Services
mattia-eleuteri Oct 2, 2026
d9e2abd
proxy: draw the masqueraded source port fully at random
mattia-eleuteri Oct 2, 2026
48b19e7
docs: record the source-port reuse seen under load and its fix
mattia-eleuteri Oct 2, 2026
c383385
main: leave the L4 table alone when told another instance owns it
mattia-eleuteri Oct 5, 2026
785522e
docs: align the RFC's L4 ruleset with what nft lists
mattia-eleuteri Oct 5, 2026
eebf7b8
docs: fix the README sentence on the announcing node
mattia-eleuteri Oct 5, 2026
6129160
docs(rfc): describe the CCM admission guard without an internal refer…
mattia-eleuteri Oct 6, 2026
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,49 @@ The nftables ruleset placed in table `ip cozy_proxy` consists of:
`allowICMP: "true"` annotation is set, the pod IP is added to
`icmp_allowed_pods` and ICMP toward it is accepted before the drop rule.

### Rule scoping

The two rewrites are deliberately not scoped alike. Every controller instance
watches every Service, but programs:

| Object | Scope | Why |
|---|---|---|
| `svc_pod` (`ingress_dnat`), `allowed_ports`, `icmp_allowed_pods` | the node hosting the backend pod | A non-hosting node would rewrite the destination before the packet has even left it. The hosting node then records a conntrack tuple of `(client -> podIP)`, while the reply leaves the pod and gets `saddr` rewritten to the service IP by `egress_snat` at priority `raw`, before conntrack. `(svcIP -> client)` matches nothing, so the reply is not `established` and `port_filter` drops it. |
| `pod_svc` (`egress_snat`) | every node | An intra-cluster client is source-NATed by the CNI to its own node address, which the overlay knows how to reach directly. The backend's reply is then tunnelled straight to that node and never traverses the hosting node's netfilter hooks, so the client's node is the only place left where the pod IP can still be turned back into the service IP. Without the entry there, the reply arrives with the wrong source and the client answers with a RST — a cross-node connection that hangs while the same-node one works. |

`NODE_NAME` carries the node identity; the chart injects it from
`spec.nodeName`. When it is unset the ingress scope check is disabled and
everything is programmed everywhere, so the binary still runs under a
deployment that does not inject it.

## L4 LoadBalancer mode (experimental)

Besides VMs, cozy-proxy can take over the public IP of ordinary `LoadBalancer`
services — several ports, several backends, port translation — so that it is
reachable from every pod the way it is from the Internet. A service opts in with
both:

```yaml
metadata:
labels:
networking.cozystack.io/lb-proxy: cozy-proxy
annotations:
service.cilium.io/type: ClusterIP # Cilium releases the VIP, keeps the ClusterIP
spec:
type: LoadBalancer
externalTrafficPolicy: Local
```

The node from which MetalLB (L2) announces the VIP DNATs new connections to its
local ready backends, masquerading only sources that are node IPs; everything
else addressed to the VIP is dropped. The mode has its own table, `ip
cozy_proxy_l4`, and never touches the VM mode's. It is off by default: run with
`--enable-l4-loadbalancer` (chart value `l4LoadBalancer.enabled`). Phase 1
handles TCP, IPv4 and `externalTrafficPolicy: Local`.

See [the RFC](docs/rfc/l4-loadbalancer-mode.md) for the model, the datapath,
the security implications and the rollout plan.

## Installation

Install controller using Helm-chart:
Expand Down
21 changes: 21 additions & 0 deletions charts/cozy-proxy/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ spec:
- name: cozy-proxy
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- $keepL4Table := and (not .Values.l4LoadBalancer.enabled) (not .Values.l4LoadBalancer.removeTableWhenDisabled) }}
{{- if or .Values.l4LoadBalancer.enabled (not .Values.vmMode.enabled) $keepL4Table }}
args:
{{- if .Values.l4LoadBalancer.enabled }}
- --enable-l4-loadbalancer
{{- end }}
{{- if $keepL4Table }}
- --remove-l4-table-when-disabled=false
{{- end }}
{{- if not .Values.vmMode.enabled }}
- --enable-vm-mode=false
{{- end }}
{{- end }}
env:
# Scopes the ingress rules to the backends hosted on this node; see
# "Rule scoping" in the README for why a non-hosting node must not
# program them.
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
securityContext:
privileged: true
capabilities:
Expand Down
11 changes: 11 additions & 0 deletions charts/cozy-proxy/templates/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,15 @@ rules:
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
{{- if .Values.l4LoadBalancer.enabled }}
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["list", "watch"]
- apiGroups: ["metallb.io"]
resources: ["servicel2statuses"]
verbs: ["list", "watch"]
{{- end }}
{{- end }}
18 changes: 18 additions & 0 deletions charts/cozy-proxy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,21 @@ daemonset:

rbac:
create: true

# L4 LoadBalancer mode: takes over from Cilium the public IP of services
# labelled networking.cozystack.io/lb-proxy=cozy-proxy that also carry
# service.cilium.io/type=ClusterIP. See docs/rfc/l4-loadbalancer-mode.md.
l4LoadBalancer:
enabled: false
# With the mode disabled, remove the table an earlier run programmed on the
# next start: this is how the mode is rolled back. Set it to false on a
# release that runs next to a second release running the L4 mode, or each of
# its starts deletes that release's table.
removeTableWhenDisabled: true

# VM mode, for services labelled service.kubernetes.io/service-proxy-name=cozy-proxy.
# Turn it off only to run the L4 mode as a second release next to a cozy-proxy
# that keeps the VM mode, for instance to try the L4 mode on a live cluster.
# That cozy-proxy then needs l4LoadBalancer.removeTableWhenDisabled=false.
vmMode:
enabled: true
Loading