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
1 change: 1 addition & 0 deletions docs/explanation/robot-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
62 changes: 43 additions & 19 deletions hcloud/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
MisconfiguredInternalIP = "MisconfiguredInternalIP"
InvalidIPv6Net = "InvalidIPv6Net"
instancesV2Subsystem = "instances_v2"
robotIPv6SubnetBits = 64
)

type instances struct {
Expand Down Expand Up @@ -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,
)
}
}

Expand All @@ -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(
Expand Down
141 changes: 141 additions & 0 deletions hcloud/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading