diff --git a/docs/explanation/robot-support.md b/docs/explanation/robot-support.md index 095fdc856..719148b5a 100644 --- a/docs/explanation/robot-support.md +++ b/docs/explanation/robot-support.md @@ -25,6 +25,7 @@ The Node controller adds information about the server to the Node object. The va - Addresses - We add the Hostname and (depending on the configuration and availability) the IPv4 and IPv6 addresses of the server in `Node.status.addresses`. - For the IPv6 address we use the first address in the Network -> For the network `2a01:f48:111:4221::` we add the address `2a01:f48:111:4221::1`. + - Robot reports the IPv6 network of a server, but not the address the server actually uses within it. If the Node object already has an IPv6 ExternalIP out of that network, we keep it instead of using the first address. - Automatic reporting of private IPs in a vSwitch to `Node.status.addresses` are not supported. - By default, we pass along InternalIPs configured via the kubelet flag `--node-ip`. This can be disabled by setting the environment variable `ROBOT_FORWARD_INTERNAL_IPS` to `false`. It is not allowed to configure the same IP for InternalIP and ExternalIP. diff --git a/hcloud/instances.go b/hcloud/instances.go index eb55e1e5a..9e84996ef 100644 --- a/hcloud/instances.go +++ b/hcloud/instances.go @@ -45,6 +45,7 @@ const ( MisconfiguredInternalIP = "MisconfiguredInternalIP" InvalidIPv6Net = "InvalidIPv6Net" instancesV2Subsystem = "instances_v2" + robotIPv6SubnetBits = 64 ) type instances struct { @@ -284,19 +285,9 @@ func robotNodeAddresses( addresses := []corev1.NodeAddress{{Type: corev1.NodeHostName, Address: server.Name}} - // Robot servers do not necessarily have an IPv6 subnet assigned, in which case the field is empty. - if ipv6 && server.ServerIPv6Net != "" { - if hostAddress := robotIPv6HostAddress(server.ServerIPv6Net); hostAddress != "" { + if ipv6 { + if hostAddress := robotIPv6ExternalIP(server, node, recorder); hostAddress != "" { addresses = append(addresses, corev1.NodeAddress{Type: corev1.NodeExternalIP, Address: hostAddress}) - } else { - utils.WarnEventLogf( - recorder, - node, - InvalidIPv6Net, - "Robot server %q reports the IPv6 subnet %q, which does not yield a valid address. As a result, no IPv6 ExternalIP is added", - server.Name, - server.ServerIPv6Net, - ) } } @@ -311,16 +302,49 @@ func robotNodeAddresses( return addresses } -func robotIPv6HostAddress(subnet string) string { - addr, err := netip.ParseAddr(subnet) - if err != nil || !addr.Is6() || addr.Is4In6() { - return "" +func robotIPv6ExternalIP( + server *hrobotmodels.Server, + node *corev1.Node, + recorder record.EventRecorder, +) string { + var address netip.Addr + + for _, nodeAddress := range node.Status.Addresses { + configured, err := netip.ParseAddr(nodeAddress.Address) + if nodeAddress.Type == corev1.NodeExternalIP && err == nil && configured.Is6() && !configured.Is4In6() { + address = configured + break + } } - hostAddress := addr.As16() - hostAddress[len(hostAddress)-1] |= 0x01 + // Robot servers do not necessarily have an IPv6 subnet assigned, in which case the field is empty. + if server.ServerIPv6Net != "" { + // Robot reports the subnet without a prefix length, e.g. 2a01:f48:111:4221:: for 2a01:f48:111:4221::/64. + var subnet netip.Prefix + if reported, err := netip.ParseAddr(server.ServerIPv6Net); err == nil && reported.Is6() && !reported.Is4In6() { + subnet = netip.PrefixFrom(reported, robotIPv6SubnetBits).Masked() + } + + switch { + case !subnet.IsValid(): + utils.WarnEventLogf( + recorder, + node, + InvalidIPv6Net, + "Robot server %q reports the IPv6 subnet %q, which is not a valid IPv6 subnet. As a result, the IPv6 ExternalIP already configured on the Node is kept instead, if there is one", + server.Name, + server.ServerIPv6Net, + ) + case !subnet.Contains(address): + address = subnet.Addr().Next() + } + } + + if !address.IsValid() { + return "" + } - return netip.AddrFrom16(hostAddress).String() + return address.String() } func appendForwardedInternalIPs( diff --git a/hcloud/instances_test.go b/hcloud/instances_test.go index 8c47ed560..4f4477bd7 100644 --- a/hcloud/instances_test.go +++ b/hcloud/instances_test.go @@ -747,6 +747,147 @@ func TestNodeAddressesRobotServer(t *testing.T) { {Type: corev1.NodeHostName, Address: "foobar"}, }, }, + { + name: "public ipv6 without IPv6 subnet keeps configured ExternalIP", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + }, + { + name: "public dual stack without IPv6 subnet keeps configured ExternalIP", + addressFamily: config.AddressFamilyDualStack, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + {Type: corev1.NodeExternalIP, Address: "203.0.113.7"}, + }, + }, + { + name: "public ipv6 keeps configured ExternalIP from the IPv6 subnet", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + ServerIPv6Net: "2001:db8:1234::", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + }, + { + name: "public dual stack keeps configured ExternalIP from the IPv6 subnet", + addressFamily: config.AddressFamilyDualStack, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + ServerIPv6Net: "2001:db8:1234::", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + {Type: corev1.NodeExternalIP, Address: "203.0.113.7"}, + }, + }, + { + name: "public ipv6 ignores configured ExternalIP outside the IPv6 subnet", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:9999::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + ServerIPv6Net: "2001:db8:1234::", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::1"}, + }, + }, + { + name: "public ipv6 keeps configured ExternalIP that is the first address of the IPv6 subnet", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::1"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + ServerIPv6Net: "2001:db8:1234::", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::1"}, + }, + }, + { + name: "public ipv6 with malformed IPv6 subnet keeps configured ExternalIP", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + ServerIPv6Net: "2001:db8:1234::/64", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + }, + { + name: "public ipv6 without IPv6 subnet ignores configured IPv4 ExternalIP", + addressFamily: config.AddressFamilyIPv6, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "203.0.113.7"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + }, + }, + { + name: "public ipv4 without IPv6 subnet ignores configured ExternalIP", + addressFamily: config.AddressFamilyIPv4, + nodeStatusNodeAddresses: []corev1.NodeAddress{ + {Type: corev1.NodeExternalIP, Address: "2001:db8:1234::5"}, + }, + server: &hrobotmodels.Server{ + Name: "foobar", + ServerIP: "203.0.113.7", + }, + expected: []corev1.NodeAddress{ + {Type: corev1.NodeHostName, Address: "foobar"}, + {Type: corev1.NodeExternalIP, Address: "203.0.113.7"}, + }, + }, } for _, test := range tests {