Skip to content

Commit 9ce26dd

Browse files
committed
Add ipv6 support for windows hyperv driver
Signed-off-by: Kartik Joshi <[email protected]>
1 parent 76bb4de commit 9ce26dd

File tree

4 files changed

+132
-5
lines changed

4 files changed

+132
-5
lines changed

pkg/minikube/driver/endpoint.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,25 @@ func ControlPlaneEndpoint(cc *config.ClusterConfig, cp *config.Node, driverName
6969
return "127.0.0.1", net.IPv4(127, 0, 0, 1), cc.APIServerPort, nil
7070
}
7171

72-
// Default: use the node IP (literal or resolvable name)
73-
host := cp.IP
72+
// Default: choose node IP by requested family; for dual keep IPv4 (back-compat).
73+
chosenNodeIP := cp.IP
74+
if strings.ToLower(cc.KubernetesConfig.IPFamily) == "ipv6" && cp.IPv6 != "" {
75+
chosenNodeIP = cp.IPv6
76+
}
77+
// Host sent back to callers may be overridden by APIServerName.
78+
host := chosenNodeIP
7479
if cc.KubernetesConfig.APIServerName != constants.APIServerName {
7580
host = cc.KubernetesConfig.APIServerName
7681
}
7782

7883
var ips []net.IP
79-
if ip := net.ParseIP(cp.IP); ip != nil {
84+
if ip := net.ParseIP(chosenNodeIP); ip != nil {
8085
ips = []net.IP{ip}
8186
} else {
8287
var err error
83-
ips, err = net.LookupIP(cp.IP)
88+
ips, err = net.LookupIP(chosenNodeIP)
8489
if err != nil || len(ips) == 0 {
85-
return host, nil, cp.Port, fmt.Errorf("failed to lookup ip for %q", cp.IP)
90+
return host, nil, cp.Port, fmt.Errorf("failed to lookup ip for %q", chosenNodeIP)
8691
}
8792
}
8893

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 The Kubernetes Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
// Small helpers to detect guest IPv4/IPv6 addresses for VM drivers (e.g., Hyper-V)
5+
// and to enforce IPv6 presence when the user requested --ip-family=ipv6/dual.
6+
7+
package guestnet
8+
9+
import (
10+
"os/exec"
11+
"strings"
12+
13+
"github.com/pkg/errors"
14+
"k8s.io/klog/v2"
15+
"k8s.io/minikube/pkg/minikube/command"
16+
"k8s.io/minikube/pkg/minikube/config"
17+
)
18+
19+
// DetectIPs returns the first global-scoped IPv4 and IPv6 found inside the guest.
20+
// It relies on standard Linux 'ip' output and ignores link-local IPv6 (fe80::/10).
21+
func DetectIPs(r command.Runner) (ipv4 string, ipv6 string, err error) {
22+
// IPv4: first global address
23+
cmd4 := exec.Command("sudo", "sh", "-c", `ip -o -4 addr show scope global | awk '{print $4}' | cut -d/ -f1 | head -n1`)
24+
rr4, err4 := r.RunCmd(cmd4)
25+
if err4 == nil {
26+
ipv4 = strings.TrimSpace(rr4.Stdout.String())
27+
} else {
28+
klog.V(2).Infof("DetectIPs: ipv4 probe failed: %v", err4)
29+
}
30+
31+
// IPv6: first global (non-link-local) address
32+
cmd6 := exec.Command("sudo", "sh", "-c", `ip -o -6 addr show scope global | awk '{print $4}' | cut -d/ -f1 | grep -v '^fe80' | head -n1`)
33+
rr6, err6 := r.RunCmd(cmd6)
34+
if err6 == nil {
35+
ipv6 = strings.TrimSpace(rr6.Stdout.String())
36+
} else {
37+
klog.V(2).Infof("DetectIPs: ipv6 probe failed: %v", err6)
38+
}
39+
40+
// Only return an error if both probes failed to execute;
41+
// missing one family is not an execution error.
42+
if err4 != nil && err6 != nil {
43+
return ipv4, ipv6, errors.Errorf("failed to probe guest IPs (ipv4: %v, ipv6: %v)", err4, err6)
44+
}
45+
return ipv4, ipv6, nil
46+
}
47+
48+
// RequireIPv6IfRequested returns an error if the cluster ip-family implies IPv6
49+
// but the guest has no detected global IPv6 address. Caller may decide to exit early.
50+
func RequireIPv6IfRequested(cc config.ClusterConfig, detectedIPv6 string) error {
51+
fam := strings.ToLower(strings.TrimSpace(cc.KubernetesConfig.IPFamily))
52+
if fam == "ipv6" || fam == "dual" {
53+
if strings.TrimSpace(detectedIPv6) == "" {
54+
return errors.Errorf(
55+
"IPv6/dual-stack requested (--ip-family=%s), but no global IPv6 was detected inside the VM. "+
56+
"Ensure your Hyper-V/VM switch provides IPv6 and the guest receives a non-link-local IPv6 address.",
57+
fam,
58+
)
59+
}
60+
}
61+
return nil
62+
}
63+

pkg/minikube/node/start.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import (
5858
"k8s.io/minikube/pkg/minikube/localpath"
5959
"k8s.io/minikube/pkg/minikube/logs"
6060
"k8s.io/minikube/pkg/minikube/machine"
61+
"k8s.io/minikube/pkg/minikube/machine/guestnet"
6162
"k8s.io/minikube/pkg/minikube/mustload"
6263
"k8s.io/minikube/pkg/minikube/out"
6364
"k8s.io/minikube/pkg/minikube/out/register"
@@ -134,6 +135,23 @@ func Start(starter Starter) (*kubeconfig.Settings, error) { // nolint:gocyclo
134135
klog.Errorf("Unable to add minikube host alias: %v", err)
135136
}
136137

138+
// Detect the guest's IPs (works for Hyper-V and other VM drivers).
139+
// This must happen before kubeadm config is generated so n.IPv6 can be used.
140+
if driver.IsVM(starter.Cfg.Driver) && !driver.IsKIC(starter.Cfg.Driver) {
141+
if ip4, ip6, _ := guestnet.DetectIPs(starter.Runner); ip6 != "" || ip4 != "" {
142+
if ip6 != "" {
143+
starter.Node.IPv6 = ip6
144+
klog.Infof("Detected node IPv6 inside guest: %s", starter.Node.IPv6)
145+
}
146+
// Fail fast if the user asked for IPv6/dual but the VM has no IPv6.
147+
guestnet.RequireIPv6IfRequested(*starter.Cfg, starter.Node.IPv6)
148+
} else {
149+
// Still enforce the requirement if requested even when detection produced nothing.
150+
guestnet.RequireIPv6IfRequested(*starter.Cfg, "")
151+
}
152+
}
153+
154+
137155
var kcs *kubeconfig.Settings
138156
var bs bootstrapper.Bootstrapper
139157
if config.IsPrimaryControlPlane(*starter.Cfg, *starter.Node) {

pkg/minikube/registry/drvs/hyperv/hyperv.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ import (
3434
"k8s.io/minikube/pkg/minikube/driver"
3535
"k8s.io/minikube/pkg/minikube/localpath"
3636
"k8s.io/minikube/pkg/minikube/registry"
37+
"k8s.io/minikube/pkg/minikube/out"
38+
"k8s.io/minikube/pkg/minikube/style"
39+
"k8s.io/klog/v2"
3740
)
3841

3942
const (
@@ -78,6 +81,22 @@ func configure(cfg config.ClusterConfig, n config.Node) (interface{}, error) {
7881
d.DiskSize = cfg.DiskSize
7982
d.SSHUser = "docker"
8083
d.DisableDynamicMemory = true // default to disable dynamic memory as minikube is unlikely to work properly with dynamic memory
84+
85+
// If the user requested IPv6/dual, warn early when the selected vSwitch likely has no IPv6 on the host vEthernet.
86+
fam := strings.ToLower(strings.TrimSpace(cfg.KubernetesConfig.IPFamily))
87+
if (fam == "ipv6" || fam == "dual") && d.VSwitch != "" {
88+
ok, err := vSwitchHasIPv6(d.VSwitch)
89+
if err != nil {
90+
klog.Warningf("IPv6 preflight for Hyper-V switch %q failed (will continue): %v", d.VSwitch, err)
91+
} else if !ok {
92+
out.WarningT("Hyper-V switch {{.sw}} appears to have no IPv6 address on the host vEthernet interface. "+
93+
"An IPv6/dual-stack cluster may fail to start. Consider using an External switch with IPv6.",
94+
out.V{"sw": d.VSwitch})
95+
out.Styled(style.Tip, `To check:
96+
PowerShell (Admin): Get-NetIPAddress -AddressFamily IPv6 | where {$_.InterfaceAlias -like "vEthernet ({{.sw}})"} | ft`,
97+
out.V{"sw": d.VSwitch})
98+
}
99+
}
81100
return d, nil
82101
}
83102

@@ -139,3 +158,25 @@ func status() registry.State {
139158

140159
return registry.State{Installed: true, Healthy: true}
141160
}
161+
162+
// vSwitchHasIPv6 returns true if the host's vEthernet (SwitchName) has any non-link-local IPv6 address.
163+
func vSwitchHasIPv6(switchName string) (bool, error) {
164+
if switchName == "" {
165+
return false, nil
166+
}
167+
ps, err := exec.LookPath("powershell")
168+
if err != nil {
169+
return false, err
170+
}
171+
iface := fmt.Sprintf("vEthernet (%s)", switchName)
172+
// True if any IPv6 other than fe80::/10 exists on the vEthernet interface for this switch.
173+
script := fmt.Sprintf(`@(Get-NetIPAddress -AddressFamily IPv6 -InterfaceAlias '%s' | Where-Object {$_.IPAddress -notlike 'fe80*'} | Select-Object -First 1) -ne $null`, iface)
174+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
175+
defer cancel()
176+
cmd := exec.CommandContext(ctx, ps, "-NoProfile", "-NonInteractive", script)
177+
out, err := cmd.CombinedOutput()
178+
if err != nil {
179+
return false, fmt.Errorf("%s failed:\n%s", strings.Join(cmd.Args, " "), out)
180+
}
181+
return strings.TrimSpace(string(out)) == "True", nil
182+
}

0 commit comments

Comments
 (0)