[vnet] Skip IPv6 setup when IPv6 is disabled on the host (#68368)

* skip IPv6 setup on Windows and Linux when IPv6 is disabled on host

* reapply DNS nameservers on Linux when they change
This commit is contained in:
Pavel
2026-07-14 16:27:06 +00:00
committed by GitHub
parent 8c17223d75
commit 12eed71266
8 changed files with 142 additions and 16 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ func runUnixAdminProcess(ctx context.Context, clt *clientApplicationServiceClien
return trace.Wrap(err, "reporting network stack info to client application")
}
osConfigProvider, err := newOSConfigProvider(osConfigProviderConfig{
osConfigProvider, err := newOSConfigProvider(ctx, osConfigProviderConfig{
clt: clt,
tunName: tunName,
ipv6Prefix: networkStackConfig.ipv6Prefix.String(),
+1 -1
View File
@@ -116,7 +116,7 @@ func runWindowsAdminProcess(ctx context.Context, cfg *windowsAdminProcessConfig)
return trace.Wrap(err, "reporting network stack info to client application")
}
osConfigProvider, err := newOSConfigProvider(osConfigProviderConfig{
osConfigProvider, err := newOSConfigProvider(ctx, osConfigProviderConfig{
clt: clt,
tunName: tunName,
ipv6Prefix: networkStackConfig.ipv6Prefix.String(),
+6 -2
View File
@@ -160,7 +160,7 @@ func (vnet *EmbeddedVNet) Run(ctx context.Context) error {
return trace.Wrap(err, "getting TUN device name")
}
osConfigProvider, err := newOSConfigProvider(osConfigProviderConfig{
osConfigProvider, err := newOSConfigProvider(ctx, osConfigProviderConfig{
clt: vnet.client,
tunName: tunName,
ipv6Prefix: stackConfig.ipv6Prefix.String(),
@@ -176,10 +176,14 @@ func (vnet *EmbeddedVNet) Run(ctx context.Context) error {
if oc.tunName == "" {
return vnet.configureHost(ctx, nil)
}
cidrRanges := oc.cidrRanges
if oc.tunIPv6 != "" {
cidrRanges = append(cidrRanges, oc.tunIPv6+"/64")
}
return vnet.configureHost(ctx, &EmbeddedVNetHostConfig{
DeviceIPv4: oc.tunIPv4,
DeviceIPv6: oc.tunIPv6,
CIDRRanges: append(oc.cidrRanges, oc.tunIPv6+"/64"),
CIDRRanges: cidrRanges,
DNSAddrs: oc.dnsAddrs,
DNSZones: oc.dnsZones,
})
+6
View File
@@ -107,6 +107,12 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo
return nil
}
// hostIPv6Disabled always returns false on macOS, there is no system-wide way
// to disable IPv6 on macOS, it can only be turned off per link.
func hostIPv6Disabled(_ /*tunName*/ string) (bool, error) {
return false, nil
}
const resolverFileComment = "# automatically installed by Teleport VNet"
var resolverPath = filepath.Join("/", "etc", "resolver")
+34 -4
View File
@@ -20,6 +20,8 @@ import (
"context"
"errors"
"net"
"os"
"path/filepath"
"slices"
"strings"
@@ -34,7 +36,7 @@ type platformOSConfigState struct {
configuredIPv6 bool
configuredIPv4 bool
configuredCidrRanges []string
configuredNameserver bool
configuredDNSAddrs []string
configuredDNSZones []string
broughtUpInterface bool
tunName string
@@ -66,7 +68,7 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo
if err := configureDNS(ctx, cfg, state); err != nil {
return trace.Wrap(err, "configuring DNS")
}
if (state.configuredIPv4 || state.configuredIPv6) && state.configuredNameserver && !state.broughtUpInterface {
if (state.configuredIPv4 || state.configuredIPv6) && len(state.configuredDNSAddrs) > 0 && !state.broughtUpInterface {
log.InfoContext(ctx, "Bringing up the VNet interface", "device", cfg.tunName)
if err := runCommand(ctx,
"ip", "link", "set", cfg.tunName, "up",
@@ -92,6 +94,34 @@ func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSCo
return nil
}
const (
// procIPv6Conf holds per-interface IPv6 settings, it is absent when the
// kernel was booted with IPv6 disabled.
procIPv6Conf = "/proc/sys/net/ipv6/conf"
// disableIPv6Setting is the per-interface setting that disables IPv6 when set to 1.
disableIPv6Setting = "disable_ipv6"
)
// hostIPv6Disabled checks whether IPv6 has been disabled on the host.
func hostIPv6Disabled(tunName string) (bool, error) {
if _, err := os.Stat(procIPv6Conf); err != nil {
if errors.Is(err, os.ErrNotExist) {
return true, nil
}
return false, trace.Wrap(err, "checking existence of %s", procIPv6Conf)
}
path := filepath.Join(procIPv6Conf, tunName, disableIPv6Setting)
disabled, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, trace.Wrap(err, "reading %s", path)
}
// The file contains "1" when IPv6 is disabled on the device, "0" when enabled
return strings.TrimSpace(string(disabled)) == "1", nil
}
func shouldReconfigureDNSZones(cfg *osConfig, state *platformOSConfigState) bool {
return !utils.ContainSameUniqueElements(cfg.dnsZones, state.configuredDNSZones)
}
@@ -148,7 +178,7 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta
state.configuredDNSZones = cfg.dnsZones
}
if len(cfg.dnsAddrs) > 0 && state.tunName != "" && !state.configuredNameserver {
if len(cfg.dnsAddrs) > 0 && state.tunName != "" && !slices.Equal(cfg.dnsAddrs, state.configuredDNSAddrs) {
iface, err := net.InterfaceByName(state.tunName)
if err != nil {
return trace.Wrap(err, "looking up interface %s", state.tunName)
@@ -171,7 +201,7 @@ func configureDNS(ctx context.Context, cfg *osConfig, state *platformOSConfigSta
if err := systemdresolved.SetLinkDNS(ctx, conn, int32(iface.Index), addresses); err != nil {
return err
}
state.configuredNameserver = true
state.configuredDNSAddrs = cfg.dnsAddrs
}
return nil
+19 -1
View File
@@ -43,13 +43,31 @@ type osConfigProviderConfig struct {
ipv6Prefix string
dnsIPv6 string
addDNSAddress func(net.IP) error
// checkHostIPv6Disabled reports whether IPv6 is disabled host-wide.
// Defaults to the platform-specific hostIPv6Disabled, overridable in tests.
checkHostIPv6Disabled func(tunName string) (bool, error)
}
type targetOSConfigGetter interface {
GetTargetOSConfiguration(context.Context) (*vnetv1.TargetOSConfiguration, error)
}
func newOSConfigProvider(cfg osConfigProviderConfig) (*osConfigProvider, error) {
func newOSConfigProvider(ctx context.Context, cfg osConfigProviderConfig) (*osConfigProvider, error) {
checkHostIPv6Disabled := cfg.checkHostIPv6Disabled
if checkHostIPv6Disabled == nil {
checkHostIPv6Disabled = hostIPv6Disabled
}
ipv6Disabled, err := checkHostIPv6Disabled(cfg.tunName)
if err != nil {
// Could not determine, assume IPv6 is enabled. If it is actually
// disabled, the address assignment will fail and the error will
// surface there.
log.WarnContext(ctx, "Failed to check whether IPv6 is disabled on the host, assuming it is enabled.",
"error", err)
} else if ipv6Disabled {
log.WarnContext(ctx, "IPv6 is disabled on this host, VNet will skip IPv6 configuration and work over IPv4 only.")
return &osConfigProvider{cfg: cfg}, nil
}
tunIPv6, err := tunIPv6ForPrefix(cfg.ipv6Prefix)
if err != nil {
return nil, trace.Wrap(err)
+41 -7
View File
@@ -35,9 +35,11 @@ func TestOSConfigProvider(t *testing.T) {
dnsIPv6 string
dnsZones []string
ipv4CIDRRanges []string
hostIPv6Disabled bool
getTargetOSConfigErr error
expectErr error
expectTargetOSConfig *osConfig
expectAddedDNSAddrs []string
}{
{
// No IPv4 address should be assigned until an IPv4 CIDR range is
@@ -73,6 +75,7 @@ func TestOSConfigProvider(t *testing.T) {
dnsZones: []string{"test.example.com"},
cidrRanges: []string{"192.168.1.0/24"},
},
expectAddedDNSAddrs: []string{"192.168.1.2"},
},
{
desc: "multiple cidr ranges",
@@ -91,6 +94,39 @@ func TestOSConfigProvider(t *testing.T) {
dnsZones: []string{"test.example.com"},
cidrRanges: []string{"10.64.0.0/10", "192.168.1.0/24"},
},
expectAddedDNSAddrs: []string{"10.64.0.2"},
},
{
// With IPv6 disabled on the host and no logged-in clusters, the
// target OS config should be empty
desc: "ipv6 disabled, no cidr ranges",
tunName: "testtun1",
ipv6Prefix: "fd01:2345:6789::",
dnsIPv6: "fd01:2345:6789::2",
hostIPv6Disabled: true,
expectTargetOSConfig: &osConfig{
tunName: "testtun1",
},
},
{
// With IPv6 disabled on the host, only the IPv4 address and the
// IPv4 nameserver should be configured.
desc: "ipv6 disabled, with cidr range",
tunName: "testtun1",
ipv6Prefix: "fd01:2345:6789::",
dnsIPv6: "fd01:2345:6789::2",
dnsZones: []string{"test.example.com"},
ipv4CIDRRanges: []string{"192.168.1.0/24"},
hostIPv6Disabled: true,
expectTargetOSConfig: &osConfig{
tunName: "testtun1",
tunIPv4: "192.168.1.1",
tunIPv4Net: &net.IPNet{IP: []byte{192, 168, 1, 0}, Mask: []byte{255, 255, 255, 0}},
dnsAddrs: []string{"192.168.1.2"},
dnsZones: []string{"test.example.com"},
cidrRanges: []string{"192.168.1.0/24"},
},
expectAddedDNSAddrs: []string{"192.168.1.2"},
},
} {
t.Run(tc.desc, func(t *testing.T) {
@@ -103,7 +139,7 @@ func TestOSConfigProvider(t *testing.T) {
}
// Keep track of new DNS addresses the osConfigProvider tried to add.
var addedDNSAddrs []string
osConfigProvider, err := newOSConfigProvider(osConfigProviderConfig{
osConfigProvider, err := newOSConfigProvider(ctx, osConfigProviderConfig{
clt: targetOSConfigGetter,
tunName: tc.tunName,
ipv6Prefix: tc.ipv6Prefix,
@@ -112,6 +148,9 @@ func TestOSConfigProvider(t *testing.T) {
addedDNSAddrs = append(addedDNSAddrs, ip.String())
return nil
},
checkHostIPv6Disabled: func(string) (bool, error) {
return tc.hostIPv6Disabled, nil
},
})
require.NoError(t, err)
@@ -122,12 +161,7 @@ func TestOSConfigProvider(t *testing.T) {
}
require.Equal(t, tc.expectTargetOSConfig, targetOSConfig)
// expectTargetOSConfig.dnsAddrs always starts with the IPv6 DNS
// addr, assert that any additional addrs were added to the network
// stack.
if len(tc.expectTargetOSConfig.dnsAddrs) > 1 {
require.ElementsMatch(t, tc.expectTargetOSConfig.dnsAddrs[1:], addedDNSAddrs)
}
require.ElementsMatch(t, tc.expectAddedDNSAddrs, addedDNSAddrs)
})
}
}
+34
View File
@@ -182,6 +182,9 @@ const (
// The UUID at the end was randomly generated once, VNet
// always writes policies at this key and cleans it up on shutdown.
vnetNRPTKeyID = `{ad074e9a-bd1b-447e-9108-14e545bf11a5}`
// This key holds host IPv6 configuration.
tcpip6ParametersKey = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
)
func configureDNS(ctx context.Context, zones, nameservers []string, doesGroupPolicyKeyExist bool) error {
@@ -217,6 +220,37 @@ func configureDNS(ctx context.Context, zones, nameservers []string, doesGroupPol
return nil
}
// hostIPv6Disabled checks whether IPv6 has been disabled on the host via the
// DisabledComponents value under tcpip6ParametersKey. The setting is
// host-wide, so the TUN device name is unused.
func hostIPv6Disabled(_ /*tunName*/ string) (bool, error) {
// Bit 0x10 disables IPv6 on all non-tunnel interfaces, checking it also
// covers the 0xFF value that disables IPv6 entirely. Absent key or value
// means that IPv6 is enabled.
//
// "Tunnel" interfaces here refers to Microsoft's tunneling protocols (6to4/ISATAP/Teredo)
// that encapsulate IPv6 packets inside IPv4. VNet's TUN device doesn't fall under this category.
//
// https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-ipv6-in-windows#ipv6-tunnel-interfaces
const disableIPv6NontunnelBit = 0x10
key, err := registry.OpenKey(registry.LOCAL_MACHINE, tcpip6ParametersKey, registry.QUERY_VALUE)
if err != nil {
if errors.Is(err, registry.ErrNotExist) {
return false, nil
}
return false, trace.Wrap(err, "opening registry key %s", tcpip6ParametersKey)
}
defer key.Close()
val, _, err := key.GetIntegerValue("DisabledComponents")
if err != nil {
if errors.Is(err, registry.ErrNotExist) {
return false, nil
}
return false, trace.Wrap(err, "reading DisabledComponents registry value")
}
return val&disableIPv6NontunnelBit != 0, nil
}
func doesKeyPathExist(k registry.Key, path string) (bool, error) {
key, err := registry.OpenKey(k, path, registry.READ)
if err != nil {