From 12eed71266079d2615ed3bb3849881aa4dba8349 Mon Sep 17 00:00:00 2001 From: Pavel <149388910+tangyatsu@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:27:06 +0100 Subject: [PATCH] [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 --- lib/vnet/admin_process_unix.go | 2 +- lib/vnet/admin_process_windows.go | 2 +- lib/vnet/embedded.go | 8 +++-- lib/vnet/osconfig_darwin.go | 6 ++++ lib/vnet/osconfig_linux.go | 38 ++++++++++++++++++++--- lib/vnet/osconfig_provider.go | 20 ++++++++++++- lib/vnet/osconfig_provider_test.go | 48 +++++++++++++++++++++++++----- lib/vnet/osconfig_windows.go | 34 +++++++++++++++++++++ 8 files changed, 142 insertions(+), 16 deletions(-) diff --git a/lib/vnet/admin_process_unix.go b/lib/vnet/admin_process_unix.go index 9c5e8127cf5..0aab7b21fc3 100644 --- a/lib/vnet/admin_process_unix.go +++ b/lib/vnet/admin_process_unix.go @@ -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(), diff --git a/lib/vnet/admin_process_windows.go b/lib/vnet/admin_process_windows.go index af1191849e7..66c0b0bb363 100644 --- a/lib/vnet/admin_process_windows.go +++ b/lib/vnet/admin_process_windows.go @@ -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(), diff --git a/lib/vnet/embedded.go b/lib/vnet/embedded.go index 9ac1b8103d5..2a428ec0e63 100644 --- a/lib/vnet/embedded.go +++ b/lib/vnet/embedded.go @@ -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, }) diff --git a/lib/vnet/osconfig_darwin.go b/lib/vnet/osconfig_darwin.go index be56e10c29f..6f551740334 100644 --- a/lib/vnet/osconfig_darwin.go +++ b/lib/vnet/osconfig_darwin.go @@ -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") diff --git a/lib/vnet/osconfig_linux.go b/lib/vnet/osconfig_linux.go index d02df9e491d..4124b2013ff 100644 --- a/lib/vnet/osconfig_linux.go +++ b/lib/vnet/osconfig_linux.go @@ -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 diff --git a/lib/vnet/osconfig_provider.go b/lib/vnet/osconfig_provider.go index f7b6a0f48fb..9cfb8c7ad6a 100644 --- a/lib/vnet/osconfig_provider.go +++ b/lib/vnet/osconfig_provider.go @@ -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) diff --git a/lib/vnet/osconfig_provider_test.go b/lib/vnet/osconfig_provider_test.go index 6889e6ca891..3767cc587e7 100644 --- a/lib/vnet/osconfig_provider_test.go +++ b/lib/vnet/osconfig_provider_test.go @@ -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) }) } } diff --git a/lib/vnet/osconfig_windows.go b/lib/vnet/osconfig_windows.go index e57306d7996..fb5cb6ee792 100644 --- a/lib/vnet/osconfig_windows.go +++ b/lib/vnet/osconfig_windows.go @@ -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 {