Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
50901b3
feat(replication): replace L2 broadcast with UDP unicast
defo89 May 1, 2026
6eb7525
feat(lab): add Kind cluster lab for PMTU replication testing
defo89 May 1, 2026
830dbe8
docs(pmtud): add openspec change for pluggable relay backends (UDP + …
rkschamer Aug 13, 2026
a4541a2
docs(pmtud): add pluggable-relay-crd openspec change (proposal, desig…
rkschamer Aug 13, 2026
70b3c26
feat(api): add PMTUNodeRelay v1alpha1 CRD types
rkschamer Aug 14, 2026
103de16
feat(relay): add Relay interface and backend factory
rkschamer Aug 14, 2026
a36fd53
fix(relay): use real cache.Cache and spec-compliant error message
rkschamer Aug 14, 2026
ccb4f55
feat(relay): add shared TUN injector and backend runnable
rkschamer Aug 14, 2026
fc5f66e
fix(relay): address code review findings (backpressure, SPDX year, er…
rkschamer Aug 14, 2026
d7421f7
feat(relay): add UDP backend behind Relay interface
rkschamer Aug 14, 2026
86cb91c
fix(relay): add Error metrics to Send, complete test coverage
rkschamer Aug 14, 2026
d184535
refactor(nflog): send captured packets via Relay interface
rkschamer Aug 14, 2026
4eb7eef
feat(relay): add CRD backend with dedup, watch-inject-delete, and TTL GC
rkschamer Aug 14, 2026
08b2550
feat(relay): add CLI flags and validation for relay backend config
rkschamer Aug 14, 2026
afe2ca6
feat(relay): add kubebuilder RBAC marker for pmtunoderelays resource
rkschamer Aug 14, 2026
800b811
feat(lab): parameterize relay backend in daemonset
rkschamer Aug 14, 2026
5f8eefd
feat(relay): wire relay backend in command.go and delete receiver pac…
rkschamer Aug 14, 2026
cba7a08
feat(ci): add label-gated E2E workflow for testing both relay backends
rkschamer Aug 14, 2026
0541279
fix(nflog): drop malformed packets instead of tearing down capture
rkschamer Aug 14, 2026
5d84a23
refactor(relay): drop dead consts, close udp send socket
rkschamer Aug 14, 2026
4188c5d
test(lab): use generated CRD directly, remove manual copy
rkschamer Aug 17, 2026
393b86d
docs(e2e): add design for Go Ginkgo e2e suite
rkschamer Aug 17, 2026
a1867b8
docs(e2e): add implementation plan with 15 bite-sized tasks
rkschamer Aug 17, 2026
0b7526c
feat(e2e): scaffold lab package types and exec helpers
rkschamer Aug 17, 2026
8af0c1f
feat(lab): add docker network provisioning
rkschamer Aug 17, 2026
0d1d186
feat(lab): add Kind cluster provisioning via Go API
rkschamer Aug 17, 2026
46a6317
fix(test): correct parseWorkerLines test input — exclude control-plane
rkschamer Aug 17, 2026
143f8a7
feat(lab): add router container provisioning
rkschamer Aug 17, 2026
442b1e2
fix(lab): check error on offload disable
rkschamer Aug 17, 2026
b6c1813
feat(lab): add route configuration and offload disabling
rkschamer Aug 17, 2026
4591f7c
feat(lab): add lifecycle Provision/Teardown
rkschamer Aug 17, 2026
7211e97
fix(lab): capture dockerExec output in Provision
rkschamer Aug 17, 2026
61cd034
feat(lab): add deploy backend (applyFile + stubs)
rkschamer Aug 17, 2026
37221a6
feat(e2e): wire lifecycle in Ginkgo BeforeSuite/AfterSuite
rkschamer Aug 17, 2026
2694dff
feat(lab): add traffic generation and PMTU inspection
rkschamer Aug 17, 2026
d51d288
feat(e2e): add PMTU convergence specs with Eventually polling
rkschamer Aug 17, 2026
1023c67
feat(e2e): add per-backend config validation specs
rkschamer Aug 17, 2026
c4fb0cb
feat(lab): add labctl CLI for manual lab management
rkschamer Aug 17, 2026
50d61a2
docs(lab): add e2e Makefile targets and CLI documentation
rkschamer Aug 17, 2026
c216a75
chore(e2e): add ginkgo/gomega dependencies
rkschamer Aug 17, 2026
6cabf16
docs(lab): remove stale bash script references in Makefile/README
rkschamer Aug 17, 2026
609f721
feat(lab): make CRD relay backend default
rkschamer Aug 17, 2026
e241b55
chore(vscode): enable e2e build tag for Go analysis
rkschamer Aug 17, 2026
95b1f6b
chore: ignore .vscode folder
rkschamer Aug 17, 2026
834dfa1
chore: remove .vscode from tracking, add to gitignore
rkschamer Aug 17, 2026
67ead50
fix(lab): correct e2e test path in Makefile
rkschamer Aug 17, 2026
b9258cd
fix(lab): change to repo root before running e2e tests
rkschamer Aug 17, 2026
3078f21
fix(lab): use REPO_ROOT env var for config paths
rkschamer Aug 17, 2026
3430416
feat(lab): stream kind/docker output to stdout/stderr
rkschamer Aug 17, 2026
0109ab0
feat(lab): add down target for cleanup
rkschamer Aug 17, 2026
03ef04f
fix(lab): fix network idempotent check
rkschamer Aug 17, 2026
951be76
fix(lab): use run() for network idempotent check, drop exec import
rkschamer Aug 17, 2026
7fbd580
fix(lab): add delay after router network connects
rkschamer Aug 17, 2026
9137dca
chore: commit fix attempts before pivot to simplified test design
rkschamer Aug 18, 2026
a60165d
docs(lab): consolidate e2e specs into single single-cluster e2e-lab spec
rkschamer Aug 18, 2026
d217913
docs(sci): consolidate e2e specs into e2e-lab, add implementation plan
rkschamer Aug 18, 2026
4b42301
feat(lab): add single-cluster kind config, drop two-cluster configs
rkschamer Aug 18, 2026
bc11cba
feat(lab): add control-plane discovery and shared node-line parser
rkschamer Aug 18, 2026
babfba0
feat(lab): single-cluster lifecycle, remove docker networks and router
rkschamer Aug 18, 2026
269e20e
feat(lab): configure control-plane as low-MTU forwarding hop
rkschamer Aug 18, 2026
2151cec
feat(lab): ping -M do trigger and route-cache MTU inspection
rkschamer Aug 18, 2026
d4f3455
feat(lab): deploy to single cluster, drop podinfo workload
rkschamer Aug 18, 2026
5a95577
test(e2e): assert single-cluster peer PMTU convergence via ping trigger
rkschamer Aug 18, 2026
7d84637
chore(lab): point down target at single cluster, drop observe-router
rkschamer Aug 18, 2026
47b716c
docs(lab): single-cluster README and real-cluster runbook
rkschamer Aug 18, 2026
f0a693b
fix e2e tets
rkschamer Aug 18, 2026
76629d1
feat(spec): remove labctl and unused CLI references
rkschamer Aug 18, 2026
fb582a5
fix: rename to repo_root again
rkschamer Aug 18, 2026
06a413a
remove duplicated CRD
rkschamer Aug 18, 2026
235fff3
remove LAB_REUSE from READme
rkschamer Aug 19, 2026
d38bbdb
revert .github to master version
rkschamer Aug 19, 2026
7efd5e0
remove .serena from project
rkschamer Aug 19, 2026
0e8a2d9
Merge branch 'master' into d053727/eliminate-l2-via-crds
rkschamer Aug 19, 2026
8d6a62f
fix fmt and linter issues
rkschamer Aug 19, 2026
d67a7cb
add missing SPDX headers
rkschamer Aug 19, 2026
2d45a68
review command.go
rkschamer Aug 19, 2026
dd01272
review config.go
rkschamer Aug 19, 2026
6cea098
switch to minimal runtime image
rkschamer Aug 19, 2026
faed5e0
clarify CalcSrcDst and add unit tests
rkschamer Aug 19, 2026
cb83ab2
review nflog/controller
rkschamer Aug 19, 2026
797f45b
address linting issues
rkschamer Aug 19, 2026
f1ebb7f
review controller_test.go
rkschamer Aug 20, 2026
ba186b8
refactor relay package
rkschamer Aug 20, 2026
4f80551
fix linting issues
rkschamer Aug 20, 2026
07a22ca
add crd backend env test + review
rkschamer Aug 20, 2026
68abb32
update readme
rkschamer Aug 20, 2026
6277acf
fixing e2e tests
rkschamer Aug 20, 2026
813c2b0
feat: differentiate metrics
rkschamer Aug 21, 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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company
#
# SPDX-License-Identifier: Apache-2.0

.git
go-pmtud
build/
.superpowers/
lab/
.github/
*.md
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
/main
/go-pmtud
/build/

.vscode/
.idea
.pi/
.superpowers/
.serena
.vscode/
95 changes: 95 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!--
SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company

SPDX-License-Identifier: Apache-2.0
-->

# Contributing

## Requirements

- Go 1.26+
- [controller-gen](https://pkg.go.dev/sigs.k8s.io/controller-tools/cmd/controller-gen) — for regenerating CRD manifests and deepcopy code
- [setup-envtest](https://pkg.go.dev/sigs.k8s.io/controller-runtime/tools/setup-envtest) — for running component tests against a real API server binary

`make prepare-static-check` installs all static analysis tools. `make install-setup-envtest` installs setup-envtest.

## Running tests

```sh
make check
```

This runs `generate`, all unit and component tests (with coverage), and the full static analysis suite.

### Unit tests only

```sh
go test ./...
```

### Component tests (envtest)

Component tests verify a single component against a real dependency spun up in-process. The CRD backend tests (`internal/relay/crd`) use [envtest](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/envtest), which starts a real Kubernetes API server binary but requires no cluster. `make check` handles this automatically via `setup-envtest`. To run them in isolation:

```sh
KUBEBUILDER_ASSETS=$(setup-envtest use 1.35 -p path) go test ./internal/relay/crd/...
```

If `KUBEBUILDER_ASSETS` is not set, the test binary prints a notice and exits cleanly (not a failure).

### Integration tests (kind)

Integration tests verify that go-pmtud works correctly inside a real multi-node cluster, including actual NFLOG capture, TUN injection, and CRD relay across nodes. They are not yet automated. To test manually with [kind](https://kind.sigs.k8s.io/):

1. Create a multi-node cluster: `kind create cluster --config kind-config.yaml` (use a config with at least two worker nodes)
2. Install the CRD: `kubectl apply -f crd/`
3. Build and load the image: `docker build -t go-pmtud . && kind load docker-image go-pmtud`
4. Deploy as a DaemonSet with `--relay-backend=crd` and the appropriate iptables init container
5. Trigger an ICMP frag-needed packet (e.g. send a large UDP packet with DF set toward a destination with a lower MTU path)
6. Verify the packet appears on other nodes via `kubectl logs` or by inspecting the route cache MTU entry on the target node

### End-to-end tests (real cluster)

End-to-end testing in a real cluster (production or staging) is performed manually before releases. It follows the same steps as the kind integration test but validates behaviour under real ECMP traffic patterns. No scripted harness exists yet.

To confirm a relay is working, check the route cache on a node that should have received the relayed packet:

```sh
ip route get <destination-IP>
# Expected: cache entry with a reduced mtu value
```

## Code generation

CRD manifests, deepcopy functions, and apply-configuration types are generated from the types in `api/v1alpha1`:

```sh
make generate
```

Run this after any change to the API types and commit the generated files.

## Static analysis

```sh
make static-check
```

Runs golangci-lint, shellcheck, typos, and license-header checks. Individual targets:

| Target | Tool |
|---|---|
| `run-golangci-lint` | golangci-lint |
| `run-shellcheck` | shellcheck |
| `run-typos` | typos spell checker |
| `check-license-headers` | addlicense + reuse |
| `check-dependency-licenses` | go-licence-detector |

## License headers

All `.go` files require an SPDX license header. To add or fix headers:

```sh
make license-headers
```
18 changes: 10 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
#
# SPDX-License-Identifier: Apache-2.0

FROM golang:1.25-alpine AS builder
FROM golang:1.26-alpine AS builder

WORKDIR /go/src/github.com/sapcc/go-pmtud
ADD go.mod go.sum ./
COPY go.mod go.sum ./
RUN go mod download
ADD . .
RUN go build -v -o /go-pmtud cmd/go-pmtud/main.go
COPY . .
RUN CGO_ENABLED=0 go build -v -o /go-pmtud cmd/go-pmtud/main.go

FROM ubuntu:noble
FROM alpine:latest AS certs
RUN apk add --no-cache ca-certificates

FROM scratch
LABEL source_repository="https://github.com/sapcc/go-pmtud"
RUN apt-get update && apt-get install -y \
iptables iproute2 \
&& rm -rf /var/lib/apt/lists/*
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go-pmtud /go-pmtud
ENTRYPOINT ["/go-pmtud"]
149 changes: 92 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,87 +25,122 @@ More details in this blog post by Cloudflare: [Path MTU discovery in practice](h

go-pmtud replicates ICMP Destination Unreachable packets to all nodes in same Kubernetes cluster, so that the sender gets awareness that it has to use smaller packets for a particular destination.

## Concept
## Architecture

1. ICMP Destination Unreachable message (type 3 code 4 message) packets are filtered and sent to specific NFlog group by iptables.
```
iptables (NFLOG group 33)
nflog controller ← captures ICMP type 3 code 4 packets
relay backend ← distributes packets to all (on other node)
TUN device (pmtud0) ← injects replicated packets into the network stack
```

2. go-pmtud replicates ICMP packets to all nodes in same Kubernetes cluster, so that the sender pod gets awareness that it has to use smaller packets for particular destination.
Each node runs go-pmtud as a DaemonSet. When an ICMP frag-needed packet arrives, iptables redirects it to an NFLOG group. go-pmtud reads it, replicates it via the configured relay backend, and re-injects it on every other node through a TUN device.

```
Exec into the pod:
## Relay Backends

# ip route get 192.168.100.10
192.168.100.10 via 192.100.0.1 dev eth0 src 192.100.0.50
cache expires 484sec mtu 9000 <<<< connection is failing
go-pmtud supports two relay backends, selected with `--relay-backend`.

# ip route get 192.168.100.10
192.168.100.10 via 192.100.0.1 dev eth0 src 192.100.0.50
cache expires 484sec mtu 8996 <<<< correct MTU information, connection is working
```
### `crd` (default)

## Build
Relay packets are stored as `PMTUNodeRelay` Kubernetes CRD objects. Every node watches the API server for new objects and injects those originating from other nodes. Expired objects are garbage-collected by each node for its own packets.

Build from source:
This backend requires no direct network connectivity between nodes — it uses the Kubernetes API server as the transport. It does require the `PMTUNodeRelay` CRD to be installed in the cluster:

```
go mod download
go build -v -o /go-pmtud cmd/go-pmtud/main.go
```sh
kubectl apply -f crd/pmtud.cloud.sap_pmtunoderelays.yaml
```

Build a Docker image:
Relay objects are namespaced; set the namespace via `--relay-namespace` or the `POD_NAMESPACE` environment variable.

```
docker build -t go-pmtud .
```
### `udp`

Relay packets are sent directly to a peer list over UDP. This is the original replication mechanism. It requires all node IPs to be provided via the node controller and needs UDP port `4390` (configurable) to be open between nodes.

## CLI Options

| Flag | Default | Description |
|---|---|---|
| `--nodename` | | Node hostname, used as a metric label |
| `--relay-backend` | `crd` | Relay backend: `crd` or `udp` |
| `--relay-namespace` | `$POD_NAMESPACE` | Namespace for CRD relay objects (`crd` backend only) |
| `--relay-gc-interval` | `60s` | How often expired CRD relay objects are garbage-collected |
| `--replication-port` | `4390` | UDP port for packet replication (`udp` backend only) |
| `--nflog_group` | `33` | NFLOG group number |
| `--ttl` | `1` | TTL of re-injected ICMP packets |
| `--ignore-networks` | | Comma-separated CIDRs — packets from these sources are not relayed |
| `--metrics_port` | `:30040` | Prometheus metrics endpoint |
| `--health_port` | `:30041` | Healthz endpoint |
| `--kube_context` | | Kubeconfig context to use |

All flags can also be set via environment variables prefixed with `PMTUD_` (e.g. `PMTUD_NODENAME`).

## go-pmtud options
## Metrics

Following options are available:
Prometheus metrics are exposed on `--metrics_port` (default `:30040`). Every metric carries a `node` label. The three stages of the relay pipeline each map to exactly one counter:

1. peers - resend ICMP frag-needed packets to this peer list.
2. iface - interface that listens for ICMP packets and resends them to other peers.
3. nodename - node hostname, used for metric label.
4. nflog-group - NFLOG group, set to 33 in our case.
5. metrics-port - Port for Prometheus metrics (30040 by default).
6. ttl - TTL of replicated ICMP packets.
7. ignore-networks - Do not resend ICMP frag-needed packets originated from specified networks
| Metric | Labels | Meaning |
|---|---|---|
| `go_pmtud_recv_packets_total` | `node`, `source_ip` | ICMP frag-needed packets **captured from the kernel** (nflog) on this node. Capture only. |
| `go_pmtud_relay_send_total` | `node`, `result` | Relay send attempts by outcome: `created`, `deduplicated`, or `error` (`crd` backend). |
| `go_pmtud_injected_packets_total` | `node`, `source` | Packets **received from a peer** and injected via the TUN device. `source` is the relaying peer (node name for `crd`, peer IP for `udp`). |

If `iface` is empty, it finds out the outgoing interface based on the default route.
Supporting metrics:

## Example - go-pmtud Daemonset
| Metric | Labels | Meaning |
|---|---|---|
| `go_pmtud_sent_packets_total` | `node` | Packets sent to peers (`udp` backend, per successful send). |
| `go_pmtud_sent_packets_peer` | `node`, `peer` | Packets sent, per peer (`udp` backend). |
| `go_pmtud_sent_error_peer_total` | `node`, `peer` | Send errors, per peer (`udp` backend). |
| `go_pmtud_error_total` | `node` | General error counter. |
| `go_pmtud_callback_duration_seconds` | `node` | Histogram of nflog callback duration. |

go-pmtud can run as a [Daemonset](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/), [example](https://github.com/sapcc/helm-charts/blob/master/system/go-pmtud).
Useful queries:

Example values.yaml:
```promql
# ICMP frag-needed captured cluster-wide, per minute
sum(rate(go_pmtud_recv_packets_total[1m])) * 60

# CR creation rate (actual etcd writes) — crd backend
sum(rate(go_pmtud_relay_send_total{result="created"}[1m])) * 60

# Deduplication ratio — how many sends collapsed onto an existing CR
sum(rate(go_pmtud_relay_send_total{result="deduplicated"}[1m]))
/ sum(rate(go_pmtud_relay_send_total{result=~"created|deduplicated"}[1m]))
```
images:
iptables:
repository: sapcc/iptables
tag: v20191226161919
pmtud:
repository: sapcc/go-pmtud
tag: latest

iptables:
nflogGroup: 33
ignoreSourceNetworks: 192.168.100.0/24

pmtud:
ttl: 10
metricsPort: 30040
interface: eth0
peers: 192.168.100.2, 192.168.100.3, 192.168.100.4, 192.168.100.5, 192.100.0.50

## Build

```sh
go mod download
go build -o go-pmtud ./cmd/go-pmtud
```

## Example - iptables and NFlog
Docker image:

There is an iptables rule on each node that redirects ICMP Destination Unreachable` packets to NFlog group nr. 33:
```sh
docker build -t go-pmtud .
```

`iptables -t raw -D PREROUTING -i <interface> -p icmp -m icmp --icmp-type 3/4 --j NFLOG --nflog-group 33`
## iptables and NFlog

Important: we need ignore packets from summarized source networks of all nodes in the local cluster to avoid re-sending loops. Use `ignore-networks` option for this.
This means a node will not re-send already retransmitted ICMP messages. It will only resend messages that are usually originated by routers on the path.
Each node needs an iptables rule that redirects ICMP Destination Unreachable packets to the NFLOG group. The rule **must** exclude the `pmtud0` TUN interface to prevent replication loops:

```sh
iptables -t raw -A PREROUTING -p icmp -m icmp --icmp-type 3/4 ! -i pmtud0 -j NFLOG --nflog-group 33
```

Optionally use `--ignore-networks` to suppress packets from known infrastructure networks (e.g. node subnets) as an additional safety layer.

## Example — DaemonSet

go-pmtud is designed to run as a [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/). A Helm chart is available at [sapcc/helm-charts](https://github.com/sapcc/helm-charts/blob/master/system/go-pmtud).

## License
This project is licensed under the Apache2 License - see the [LICENSE](LICENSE) file for details

This project is licensed under the Apache 2.0 License — see the [LICENSE](LICENSE) file for details.
9 changes: 9 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ path = [
]
SPDX-FileCopyrightText = "SAP SE or an SAP affiliate company"
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = [
"api/v1alpha1/zz_generated.deepcopy.go",
"config/rbac/role.yaml",
"crd/pmtud.cloud.sap_pmtunoderelays.yaml",
]
SPDX-FileCopyrightText = "SAP SE or an SAP affiliate company"
SPDX-License-Identifier = "Apache-2.0"
17 changes: 17 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company
// SPDX-License-Identifier: Apache-2.0

// +kubebuilder:object:generate=true
// +groupName=pmtud.cloud.sap
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
GroupVersion = schema.GroupVersion{Group: "pmtud.cloud.sap", Version: "v1alpha1"}
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
AddToScheme = SchemeBuilder.AddToScheme
)
Loading
Loading