Skip to content

Commit 0cba535

Browse files
authored
fix(routes): reject IPv6 routes early (#1280)
Detect IPv6 destination CIDRs up front and return a clear error without issuing an API call. Also document that the route controller manages IPv4 routes exclusively. Fixes #659
1 parent 6e13859 commit 0cba535

3 files changed

Lines changed: 46 additions & 8 deletions

File tree

docs/explanation/private-networks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ It is enabled by default, if a networking is enabled and a Private Network ID or
2727

2828
To utilize this feature you need to use a CNI, which supports using the native routing capability of the infrastructure. As an example, Cilium can be set to use the [`routing-mode: native`](https://docs.cilium.io/en/stable/network/concepts/routing/#native-routing).
2929

30+
Private Networks only support IPv4, so the route controller manages IPv4 routes exclusively. In a dual-stack cluster it will not create routes for IPv6 pod CIDRs; IPv6 connectivity must be handled separately (e.g. natively by your CNI).
31+
3032
### IP Range Considerations
3133

3234
When using the route controller you need to make some considerations on the IP ranges for the cluster and service CIDR. By default, Kubernetes will allocate a `/24` subnet for each node. Depending on the amount of nodes you plan to add to the cluster you need to choose your cluster CIDR accordingly.

hcloud/routes.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ func (r *routes) CreateRoute(ctx context.Context, _ string, _ string, route *clo
129129
metrics.OperationCalled.WithLabelValues(op).Inc()
130130
ctx = cache.SetSubsystem(ctx, "routes")
131131

132+
// Parse and return early if we detect IPv6 routes.
133+
// Private Networks don't support IPv6, so we can save an API
134+
// request by validating beforehand.
135+
ip, ipNet, err := net.ParseCIDR(route.DestinationCIDR)
136+
if err != nil {
137+
return fmt.Errorf("%s: %w", op, err)
138+
}
139+
if ip.To4() == nil {
140+
return fmt.Errorf(
141+
"%s: can't create route %q via node %q: private networks do not support IPv6",
142+
op,
143+
ipNet.String(),
144+
route.TargetNode,
145+
)
146+
}
147+
132148
node, gateway, err := r.resolveRouteTarget(ctx, string(route.TargetNode))
133149
if err != nil {
134150
return fmt.Errorf("%s: error resolving route target: %w", op, err)
@@ -140,15 +156,10 @@ func (r *routes) CreateRoute(ctx context.Context, _ string, _ string, route *clo
140156
return fmt.Errorf("%s: IP %s not part of routes target addresses", op, gateway.String())
141157
}
142158

143-
_, cidr, err := net.ParseCIDR(route.DestinationCIDR)
144-
if err != nil {
145-
return fmt.Errorf("%s: %w", op, err)
146-
}
147-
148-
r.warnCIDRMismatch(cidr, node)
159+
r.warnCIDRMismatch(ipNet, node)
149160

150-
if err := r.upsertRoute(ctx, gateway, cidr, string(route.TargetNode)); err != nil {
151-
return fmt.Errorf("error upserting route %q via %q: %w", cidr.String(), gateway.String(), err)
161+
if err := r.upsertRoute(ctx, gateway, ipNet, string(route.TargetNode)); err != nil {
162+
return fmt.Errorf("error upserting route %q via %q: %w", ipNet.String(), gateway.String(), err)
152163
}
153164

154165
return nil

hcloud/routes_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,31 @@ func TestRoutes_CreateRoute_RobotProviderID(t *testing.T) {
253253
assert.ErrorContains(t, err, "not a cloud server")
254254
}
255255

256+
// TestRoutes_CreateRoute_IPv6 asserts that an IPv6 destination CIDR is rejected early with a
257+
// clear error.
258+
func TestRoutes_CreateRoute_IPv6(t *testing.T) {
259+
env := newTestEnv()
260+
defer env.Teardown()
261+
262+
env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, _ *http.Request) {
263+
json.NewEncoder(w).Encode(schema.NetworkGetResponse{
264+
Network: schema.Network{ID: 1, Name: "network-1", IPRange: "10.0.0.0/8"},
265+
})
266+
})
267+
268+
routes, err := newRoutes(env.Client, 1, DefaultClusterCIDR, env.Recorder, nodeLister(t), env.ServerCache)
269+
require.NoError(t, err)
270+
271+
err = routes.CreateRoute(context.TODO(), "my-cluster", "route", &cloudprovider.Route{
272+
TargetNode: "node15",
273+
DestinationCIDR: "2001:cafe:42::/64",
274+
})
275+
require.Error(t, err)
276+
assert.ErrorContains(t, err, "private networks do not support IPv6")
277+
// The target node must be named in the error, not left blank (route.Name is unset here).
278+
assert.ErrorContains(t, err, "node15")
279+
}
280+
256281
// TestRoutes_CreateRoute_NodeNameDrift proves the routes controller still works when the
257282
// k8s node name differs from the hcloud server name — the core fix in this PR. The server is
258283
// resolved by its immutable ProviderID rather than by k8s node name.

0 commit comments

Comments
 (0)