Files
teleport/lib/vnet/network_stack.go
Pavel 575189ac66 [vnet] Skip AAAA records when IPv6 is disabled on the host (#68666)
* don't advertise IPv6 in VNet when IPv6 is disabled on host

* show ipv6 disabled in VNet diag report

* add platformHostIPv6Disabled stub for unsupported OSes

* fix some nits
2026-08-10 18:12:19 +00:00

914 lines
31 KiB
Go

// Teleport
// Copyright (C) 2024 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package vnet
import (
"context"
"errors"
"log/slog"
"net"
"net/netip"
"os"
"strconv"
"sync"
"sync/atomic"
"github.com/gravitational/trace"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/singleflight"
"golang.zx2c4.com/wireguard/device"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
ipv4network "gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
ipv6network "gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"gvisor.dev/gvisor/pkg/waiter"
"github.com/gravitational/teleport"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/vnet/dns"
)
var log = logutils.NewPackageLogger(teleport.ComponentKey, logComponent)
const (
logComponent = "vnet"
dnsLogComponent = "dns"
nicID = 1
// vnetTUNMTU is the default MTU for the TUN device. It can be overridden
// with the TELEPORT_UNSTABLE_VNET_TUN_MTU environment variable.
vnetTUNMTU = 16 * 1024
// vnetTUNMTUEnvVar overrides the default TUN device MTU. It must be set in
// the environment of the VNet admin process.
vnetTUNMTUEnvVar = "TELEPORT_UNSTABLE_VNET_TUN_MTU"
// minTUNMTU is the lowest accepted MTU override, chosen because IPv6
// requires a minimum link MTU of 1280 bytes (RFC 8200).
minTUNMTU = 1280
// maxTUNMTU is the largest possible IP packet size, the 16-bit total
// length field limit shared by IPv4 (RFC 791) and IPv6 (RFC 8200).
maxTUNMTU = 65535
tcpReceiveBufferSize = 0 // 0 means a default will be used.
maxInFlightTCPConnectionAttempts = 1024
)
// tunMTU returns the MTU to use for the TUN device.
func tunMTU(ctx context.Context) int {
env := os.Getenv(vnetTUNMTUEnvVar)
if env == "" {
return vnetTUNMTU
}
mtu, err := strconv.Atoi(env)
if err != nil || mtu < minTUNMTU || mtu > maxTUNMTU {
log.WarnContext(ctx, "Ignoring invalid TUN MTU override.",
"env_var", vnetTUNMTUEnvVar, "value", env, "min", minTUNMTU, "max", maxTUNMTU)
return vnetTUNMTU
}
log.InfoContext(ctx, "Using TUN MTU override from environment.",
"env_var", vnetTUNMTUEnvVar, "mtu", mtu)
return mtu
}
// networkStackConfig holds configuration parameters for the VNet network stack.
type networkStackConfig struct {
// tunDevice is the OS TUN virtual network interface.
tunDevice TUNDevice
// ipv6Prefix is the IPv6 ULA prefix to use for all assigned VNet IP addresses.
ipv6Prefix tcpip.Address
// dnsIPv6 is the IPv6 address on which to host the DNS server. It must be under IPv6Prefix.
dnsIPv6 tcpip.Address
// ipv6Disabled is true when IPv6 is disabled on the host. The network
// stack keeps its internal IPv6 prefix, but VNet doesn't answer AAAA
// queries, has no IPv6 DNS address, and doesn't register the IPv6
// prefix on its TUN device.
ipv6Disabled bool
// tcpHandlerResolver will be used to resolve all DNS queries that VNet may
// need to handle.
tcpHandlerResolver *tcpHandlerResolver
// upstreamNameserverSource, if set, overrides the default OS UpstreamNameserverSource which provides the
// IP addresses that unmatched DNS queries should be forwarded to. It is used in tests.
upstreamNameserverSource dns.UpstreamNameserverSource
}
// checkAndSetDefaults checks the config and sets defaults.
func (c *networkStackConfig) checkAndSetDefaults() error {
if c.tunDevice == nil {
return trace.BadParameter("tunDevice is required")
}
if c.ipv6Prefix.Len() != 16 || c.ipv6Prefix.AsSlice()[0] != 0xfd {
return trace.BadParameter("ipv6Prefix must be an IPv6 ULA address")
}
if !c.ipv6Disabled && c.dnsIPv6 == (tcpip.Address{}) {
return trace.BadParameter("dnsIPv6 is required when IPv6 is enabled")
}
if c.tcpHandlerResolver == nil {
return trace.BadParameter("tcpHandlerResolver is required")
}
return nil
}
// getIPv6Prefix returns the IPv6 prefix to expose outside the network stack:
// empty when IPv6 is disabled on the host, even though ipv6Prefix is always
// set internally. Use it whenever the prefix leaves the network stack.
func (c *networkStackConfig) getIPv6Prefix() string {
if c.ipv6Disabled {
return ""
}
return c.ipv6Prefix.String()
}
// errNoTCPHandler should be returned by tcpHandlerResolvers when no handler
// matches the FQDN.
//
// Avoid using [trace.Wrap] on errNoTCPHandler where possible, this isn't an
// unexpected error that should require the overhead of collecting a stack trace.
var errNoTCPHandler = &trace.NotFoundError{Message: "no handler for address"}
// tcpHandlerSpec specifies a VNet TCP handler.
type tcpHandlerSpec struct {
// ipv4CIDRRange is the network that any V4 IP address should be assigned to this handler from.
ipv4CIDRRange string
// tcpHandler is the handler for TCP connections.
tcpHandler tcpHandler
}
// tcpHandler defines the behavior for handling TCP connections from VNet.
//
// Implementations should attempt to dial the target and return any errors before calling
// [connector] to complete the TCP handshake and get the TCP conn. This is so that clients will see that the
// TCP connection was refused, instead of seeing a successful TCP dial that is immediately closed.
type tcpHandler interface {
handleTCPConnector(ctx context.Context, localPort uint16, connector func() (net.Conn, error)) error
}
// udpHandler defines the behavior for handling UDP connections from VNet.
type udpHandler interface {
HandleUDP(context.Context, net.Conn) error
}
// TUNDevice abstracts a virtual network TUN device.
type TUNDevice interface {
// Name returns the current name of the Device.
Name() (string, error)
// MTU returns the MTU of the Device. The network stack sizes its link
// endpoint and packet buffers from this value, so it must not change over
// the lifetime of a Device.
MTU() (int, error)
// Write one or more packets to the device (without any additional headers).
// On a successful write it returns the number of packets written. A nonzero
// offset can be used to instruct the Device on where to begin writing from
// each packet contained within the bufs slice.
Write(bufs [][]byte, offset int) (int, error)
// Read one or more packets from the Device (without any additional headers).
// On a successful read it returns the number of packets read, and sets
// packet lengths within the sizes slice. len(sizes) must be >= len(bufs).
// A nonzero offset can be used to instruct the Device on where to begin
// reading into each element of the bufs slice.
Read(bufs [][]byte, sizes []int, offset int) (n int, err error)
// BatchSize returns the preferred/max number of packets that can be read or
// written in a single read/write call. BatchSize must not change over the
// lifetime of a Device.
BatchSize() int
// Close stops the Device and closes the Event channel.
Close() error
}
// networkStack implements the TCP and UDP networking stack for VNet.
type networkStack struct {
// stack is the gVisor networking stack.
stack *stack.Stack
// tun is the OS TUN device. Incoming IP/L3 packets will be copied from here to [linkEndpoint], and
// outgoing packets from [linkEndpoint] will be written here.
tun TUNDevice
// linkEndpoint is the gVisor-side endpoint that emulates the OS TUN device. All incoming IP/L3 packets
// from the OS TUN device will be injected as inbound packets to this endpoint to be processed by the
// gVisor netstack which ultimately calls the TCP or UDP protocol handler. When the protocol handler
// writes packets to the gVisor stack to an address assigned to this endpoint, they will be written to
// this endpoint, and then copied from this endpoint to the OS TUN device.
linkEndpoint *channel.Endpoint
// ipv6Prefix holds the 96-bit prefix that will be used for all IPv6 addresses assigned in the VNet.
ipv6Prefix tcpip.Address
// ipv6Disabled is true when IPv6 is disabled on the host. The network
// stack keeps its internal IPv6 prefix, but VNet doesn't answer AAAA
// queries, has no IPv6 DNS address, and doesn't register the IPv6
// prefix on its TUN device.
ipv6Disabled bool
// diagProbeIPv6 is the IPv6 address (ipv6Prefix::2) returned to diagnostic probe queries.
// Set once in newNetworkStack.
diagProbeIPv6 [16]byte
// diagProbeIPv4 is the IPv4 address returned to diagnostic probe queries. Set on the
// first call of addDNSAddress, which happens after the first call of targetOSConfig
// and before DNS addresses are registered with any OS resolver, so ResolveA should
// not read nil in practice.
diagProbeIPv4 atomic.Pointer[[4]byte]
// dnsServer is the VNet's local DNS server that can handle UDP DNS
// requests.
dnsServer *dns.Server
// upstreamFilter removes VNet DNS addresses from upstream nameserver lists.
upstreamFilter *filteredUpstreamSource
// tcpHandlerResolver resolves FQDNs to a TCP handler that will be used to handle all future TCP
// connections to IP addresses that will be assigned to that FQDN.
tcpHandlerResolver *tcpHandlerResolver
// resolveHandlerGroup is a [singleflight.Group] that will be used to avoid resolving the same FQDN
// multiple times concurrently. Every call to [tcpHandlerResolver.ResolveTCPHandler] will be wrapped by
// this. The key will be the FQDN.
resolveHandlerGroup singleflight.Group
// destroyed is a channel that will be closed when the VNet is in the process of being destroyed.
// All goroutines should terminate quickly after either this is closed or the context passed to
// [networkStack.Run] is canceled.
destroyed chan struct{}
// wg is a [sync.WaitGroup] that keeps track of all running goroutines started by the [networkStack].
wg sync.WaitGroup
// state holds all mutable state for the networkStack.
state state
slog *slog.Logger
}
type filteredUpstreamSource struct {
base dns.UpstreamNameserverSource
mu sync.RWMutex
exclude map[string]struct{}
slog *slog.Logger
}
// filteredUpstreamSource wraps an upstream source and excludes addresses added via AddExclude.
// It is mainly used to filter VNet's own DNS addresses.
func newFilteredUpstreamSource(base dns.UpstreamNameserverSource, slog *slog.Logger) *filteredUpstreamSource {
return &filteredUpstreamSource{
base: base,
exclude: make(map[string]struct{}),
slog: slog,
}
}
func (f *filteredUpstreamSource) AddExclude(addr string) {
if addr == "" {
return
}
f.mu.Lock()
defer f.mu.Unlock()
f.exclude[addr] = struct{}{}
}
func (f *filteredUpstreamSource) UpstreamNameservers(ctx context.Context) ([]string, error) {
nameservers, err := f.base.UpstreamNameservers(ctx)
if err != nil {
return nil, err
}
f.slog.DebugContext(ctx, "Loaded upstream nameservers (pre-filter)", "nameservers", nameservers)
f.mu.RLock()
defer f.mu.RUnlock()
if len(f.exclude) == 0 {
f.slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", nameservers)
return nameservers, nil
}
filtered := make([]string, 0, len(nameservers))
for _, nameserver := range nameservers {
if _, ok := f.exclude[nameserver]; ok {
continue
}
filtered = append(filtered, nameserver)
}
f.slog.DebugContext(ctx, "Loaded upstream nameservers (post-filter)", "nameservers", filtered)
return filtered, nil
}
type state struct {
// mu is a single mutex that protects the whole state struct. This could be optimized as necessary.
mu sync.RWMutex
// Each FQDN gets assigned both an IPv4 address and an IPv6 address, where the 4-bit suffix of the IPv6
// matches the IPv4 address exactly. All per-FQDN state references the smaller IPv4 address only and
// lookups based on an IPv6 address can use the 4-byte suffix.
// tcpHandlers holds the map of IP addresses to assigned TCP handlers.
tcpHandlers map[ipv4]tcpHandler
// assignedIPs holds the map of app FQDNs to their assigned IP address, it's a reverse map of [tcpHandlers].
assignedIPs map[string]ipv4
// udpHandlers holds the map of IP addresses to assigned UDP handlers.
udpHandlers map[ipv4]udpHandler
}
func newState() state {
return state{
tcpHandlers: make(map[ipv4]tcpHandler),
udpHandlers: make(map[ipv4]udpHandler),
assignedIPs: make(map[string]ipv4),
}
}
// newNetworkStack creates a new VNet network stack with the given configuration and root context.
// It takes ownership of [cfg.TUNDevice] and will handle closing it before Run() returns. Call Run()
// on the returned network stack to start the VNet.
func newNetworkStack(cfg *networkStackConfig) (*networkStack, error) {
if err := cfg.checkAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
logger := slog.With(teleport.ComponentKey, logComponent)
dnsLogger := slog.With(teleport.ComponentKey, teleport.Component(logComponent, dnsLogComponent))
mtu, err := cfg.tunDevice.MTU()
if err != nil {
return nil, trace.Wrap(err, "getting TUN device MTU")
}
stack, linkEndpoint, err := createStack(uint32(mtu))
if err != nil {
return nil, trace.Wrap(err)
}
if err := installVnetRoutes(stack); err != nil {
return nil, trace.Wrap(err)
}
ns := &networkStack{
tun: cfg.tunDevice,
stack: stack,
linkEndpoint: linkEndpoint,
ipv6Prefix: cfg.ipv6Prefix,
ipv6Disabled: cfg.ipv6Disabled,
tcpHandlerResolver: cfg.tcpHandlerResolver,
destroyed: make(chan struct{}),
state: newState(),
slog: logger,
}
upstreamNameserverSource := cfg.upstreamNameserverSource
if upstreamNameserverSource == nil {
upstreamNameserverSource, err = dns.NewOSUpstreamNameserverSource(dnsLogger)
if err != nil {
return nil, trace.Wrap(err)
}
}
upstreamFilter := newFilteredUpstreamSource(upstreamNameserverSource, dnsLogger)
ns.upstreamFilter = upstreamFilter
if cfg.dnsIPv6 != (tcpip.Address{}) {
addr, ok := netip.AddrFromSlice(cfg.dnsIPv6.AsSlice())
if !ok {
return nil, trace.Errorf("error parsing IPv6 DNS address %v", cfg.dnsIPv6)
}
upstreamFilter.AddExclude(dns.AddrWithDNSPort(addr))
}
dnsServer, err := dns.NewServer(ns, upstreamFilter, dnsLogger)
if err != nil {
return nil, trace.Wrap(err)
}
ns.dnsServer = dnsServer
tcpForwarder := tcp.NewForwarder(ns.stack, tcpReceiveBufferSize, maxInFlightTCPConnectionAttempts, ns.handleTCP)
ns.stack.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
udpForwarder := udp.NewForwarder(ns.stack, ns.handleUDP)
ns.stack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
if cfg.dnsIPv6 != (tcpip.Address{}) {
ns.diagProbeIPv6 = cfg.dnsIPv6.As16()
if err := ns.assignUDPHandler(cfg.dnsIPv6, dnsServer); err != nil {
return nil, trace.Wrap(err)
}
logger.DebugContext(context.Background(), "Serving DNS on IPv6.", "dns_addr", cfg.dnsIPv6)
} else {
// Added it so the probe handler can always return a stable value
ns.diagProbeIPv6 = ipv6WithSuffix(cfg.ipv6Prefix, dns.DNSServerSuffix).As16()
}
return ns, nil
}
func createStack(mtu uint32) (*stack.Stack, *channel.Endpoint, error) {
netStack := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4network.NewProtocol, ipv6network.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol},
})
const (
size = 512
linkAddr = ""
)
linkEndpoint := channel.New(size, mtu, linkAddr)
if err := netStack.CreateNIC(nicID, linkEndpoint); err != nil {
return nil, nil, trace.Errorf("creating VNet NIC: %s", err)
}
return netStack, linkEndpoint, nil
}
func installVnetRoutes(stack *stack.Stack) error {
// Make the network stack pass all outbound IP packets to the NIC, regardless of destination IP address.
ipv4Subnet, err := tcpip.NewSubnet(tcpip.AddrFrom4([4]byte{}), tcpip.MaskFromBytes(make([]byte, 4)))
if err != nil {
return trace.Wrap(err, "creating VNet IPv4 subnet")
}
ipv6Subnet, err := tcpip.NewSubnet(tcpip.AddrFrom16([16]byte{}), tcpip.MaskFromBytes(make([]byte, 16)))
if err != nil {
return trace.Wrap(err, "creating VNet IPv6 subnet")
}
stack.SetRouteTable([]tcpip.Route{
{
Destination: ipv4Subnet,
NIC: nicID,
},
{
Destination: ipv6Subnet,
NIC: nicID,
},
})
return nil
}
// run starts the VNet networking stack. It blocks until [ctx] is canceled, at
// which point it closes the link endpoint, waits for all goroutines to
// terminate, and destroys the networking stack.
func (ns *networkStack) run(ctx context.Context) error {
ns.slog.InfoContext(ctx, "Running Teleport VNet.", "ipv6_prefix", ns.ipv6Prefix)
ctx, cancel := context.WithCancel(ctx)
allErrors := make(chan error, 2)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
// Make sure to cancel the context in case this exits prematurely with a nil error.
defer cancel()
err := forwardBetweenTunAndNetstack(ctx, ns.tun, ns.linkEndpoint)
allErrors <- err
return err
})
g.Go(func() error {
// When the context is canceled for any reason (the caller or one of the other concurrent tasks may
// have canceled it) destroy everything and quit.
<-ctx.Done()
ns.slog.InfoContext(ctx, "Context canceled, beginning network stack shutdown.")
// In-flight connections should start terminating after closing [ns.destroyed].
close(ns.destroyed)
// Close the link endpoint and the TUN, this should cause [forwardBetweenTunAndNetstack] to terminate
// if it hasn't already.
ns.slog.InfoContext(ctx, "Closing link endpoint.")
ns.linkEndpoint.Close()
ns.slog.InfoContext(ctx, "Closing TUN device.")
err := trace.Wrap(ns.tun.Close(), "closing TUN device")
allErrors <- err
return err
})
// Deliberately ignoring the error from g.Wait() to return an aggregate of all errors.
_ = g.Wait()
// Wait for all connections and goroutines to clean themselves up.
ns.slog.InfoContext(ctx, "Waiting for all connections and goroutines to clean up.")
ns.wg.Wait()
// Now we can destroy the gVisor networking stack and wait for all its goroutines to terminate.
ns.slog.InfoContext(ctx, "Destroying networking stack.")
ns.stack.Destroy()
close(allErrors)
return trace.NewAggregateFromChannel(allErrors, context.Background())
}
func (ns *networkStack) handleTCP(req *tcp.ForwarderRequest) {
// Add 1 to the waitgroup because the networking stack runs this in its own goroutine.
ns.wg.Add(1)
defer ns.wg.Done()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Clients of *tcp.ForwarderRequest must eventually call Complete on it exactly once.
// [req] consumes 1 of [maxInFlightTCPConnectionAttempts] until [req.Complete] is called.
var completed bool
defer func() {
if !completed {
req.Complete(true /*send TCP reset*/)
}
}()
id := req.ID()
slog := ns.slog.With("request", id)
slog.DebugContext(ctx, "Handling TCP connection.")
defer slog.DebugContext(ctx, "Finished handling TCP connection.")
handler, ok := ns.getTCPHandler(id.LocalAddress)
if !ok {
slog.DebugContext(ctx, "No handler for address.", "addr", id.LocalAddress)
return
}
connector := func() (net.Conn, error) {
var wq waiter.Queue
waitEntry, notifyCh := waiter.NewChannelEntry(waiter.EventErr | waiter.EventHUp)
wq.EventRegister(&waitEntry)
endpoint, err := req.CreateEndpoint(&wq)
if err != nil {
// This err doesn't actually implement [error]
return nil, trace.Errorf("creating TCP endpoint: %s", err)
}
completed = true
req.Complete(false /*don't send TCP reset*/)
endpoint.SocketOptions().SetKeepAlive(true)
conn := gonet.NewTCPConn(&wq, endpoint)
ns.wg.Add(1)
go func() {
defer func() {
cancel()
conn.Close()
ns.wg.Done()
}()
select {
case <-notifyCh:
slog.DebugContext(ctx, "Got HUP or ERR, canceling request context and closing TCP conn.")
case <-ns.destroyed:
slog.DebugContext(ctx, "VNet is being destroyed, canceling request context and closing TCP conn.")
case <-ctx.Done():
slog.DebugContext(ctx, "Request context canceled, closing TCP conn.")
}
}()
return conn, nil
}
if err := handler.handleTCPConnector(ctx, id.LocalPort, connector); err != nil {
if errors.Is(err, context.Canceled) {
slog.DebugContext(ctx, "TCP connection handler returned early due to canceled context.")
} else {
slog.DebugContext(ctx, "Error handling TCP connection.", "err", err)
}
}
}
func (ns *networkStack) getTCPHandler(addr tcpip.Address) (tcpHandler, bool) {
ns.state.mu.RLock()
defer ns.state.mu.RUnlock()
handler, ok := ns.state.tcpHandlers[ipv4Suffix(addr)]
return handler, ok
}
// assignTCPHandler assigns an IPv4 address to [handlerSpec] from its preferred CIDR range, and returns that
// new assigned address.
func (ns *networkStack) assignTCPHandler(handlerSpec *tcpHandlerSpec, fqdn string) (ipv4, error) {
_, ipNet, err := net.ParseCIDR(handlerSpec.ipv4CIDRRange)
if err != nil {
return 0, trace.Wrap(err, "parsing CIDR %q", handlerSpec.ipv4CIDRRange)
}
ns.state.mu.Lock()
defer ns.state.mu.Unlock()
ip, err := randomFreeIPv4InNet(ipNet, func(ip ipv4) bool {
_, taken := ns.state.tcpHandlers[ip]
return !taken
})
if err != nil {
return 0, trace.Wrap(err, "assigning IP address")
}
ns.state.tcpHandlers[ip] = handlerSpec.tcpHandler
ns.state.assignedIPs[fqdn] = ip
if err := ns.addProtocolAddress(tcpip.AddrFrom4(ip.asArray())); err != nil {
return 0, trace.Wrap(err)
}
if err := ns.addProtocolAddress(ipv6WithSuffix(ns.ipv6Prefix, ip.asSlice())); err != nil {
return 0, trace.Wrap(err)
}
return ip, nil
}
func (ns *networkStack) handleUDP(req *udp.ForwarderRequest) bool {
ns.wg.Add(1)
go func() {
defer ns.wg.Done()
ns.handleUDPConcurrent(req)
}()
return true
}
func (ns *networkStack) handleUDPConcurrent(req *udp.ForwarderRequest) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
id := req.ID()
slog := ns.slog.With("request", id)
slog.DebugContext(ctx, "Handling UDP request.")
defer slog.DebugContext(ctx, "Finished handling UDP request.")
handler, ok := ns.getUDPHandler(id.LocalAddress)
if !ok {
slog.DebugContext(ctx, "No handler for address.")
return
}
var wq waiter.Queue
waitEntry, notifyCh := waiter.NewChannelEntry(waiter.EventErr | waiter.EventHUp)
wq.EventRegister(&waitEntry)
endpoint, err := req.CreateEndpoint(&wq)
if err != nil {
slog.ErrorContext(ctx, "Failed to create UDP endpoint.", "error", err)
return
}
conn := gonet.NewUDPConn(&wq, endpoint)
defer conn.Close()
ns.wg.Add(1)
go func() {
defer func() {
cancel()
conn.Close()
ns.wg.Done()
}()
select {
case <-notifyCh:
slog.DebugContext(ctx, "Got HUP or ERR, canceling request context and closing UDP conn.")
case <-ns.destroyed:
slog.DebugContext(ctx, "VNet is being destroyed, canceling request context and closing UDP conn.")
case <-ctx.Done():
slog.DebugContext(ctx, "Request context canceled, closing UDP conn.")
}
}()
if err := handler.HandleUDP(ctx, conn); err != nil {
slog.DebugContext(ctx, "Error handling UDP conn.", "error", err)
}
}
func (ns *networkStack) getUDPHandler(addr tcpip.Address) (udpHandler, bool) {
ipv4 := ipv4Suffix(addr)
ns.state.mu.RLock()
defer ns.state.mu.RUnlock()
handler, ok := ns.state.udpHandlers[ipv4]
return handler, ok
}
func (ns *networkStack) assignUDPHandler(addr tcpip.Address, handler udpHandler) error {
ipv4 := ipv4Suffix(addr)
ns.state.mu.Lock()
defer ns.state.mu.Unlock()
if _, ok := ns.state.udpHandlers[ipv4]; ok {
return trace.AlreadyExists("Handler for %s is already set", addr)
}
if err := ns.addProtocolAddress(addr); err != nil {
return trace.Wrap(err)
}
ns.state.udpHandlers[ipv4] = handler
return nil
}
// addDNSAddress adds a DNS handler at the given IP.
func (ns *networkStack) addDNSAddress(ip net.IP) error {
slog.DebugContext(context.Background(), "Serving DNS on IPv4.", "dns_addr", ip.String())
addr, ok := netip.AddrFromSlice(ip)
if !ok {
return trace.Errorf("error parsing IPv4 DNS address %s", ip.String())
}
if v4 := ip.To4(); v4 != nil && ns.diagProbeIPv4.Load() == nil {
var b [4]byte
copy(b[:], v4)
ns.diagProbeIPv4.CompareAndSwap(nil, &b)
}
if ns.upstreamFilter != nil {
ns.upstreamFilter.AddExclude(dns.AddrWithDNSPort(addr))
}
return trace.Wrap(ns.assignUDPHandler(tcpip.AddrFromSlice(ip), ns.dnsServer),
"adding UDP handler at %s", ip.String())
}
// ResolveA implements [dns.Resolver.ResolveA].
func (ns *networkStack) ResolveA(ctx context.Context, fqdn string) (dns.Result, error) {
// Diagnostic probes short-circuit here. Without this, each probe's unique random label
// would allocate a fresh IPv4 from the CIDR pool, leaking the entry.
if dns.HasDiagProbePrefix(fqdn) {
if v4 := ns.diagProbeIPv4.Load(); v4 != nil {
return dns.Result{A: *v4}, nil
}
return dns.Result{NoRecord: true}, nil
}
// Do the actual resolution within a [singleflight.Group] keyed by [fqdn] to avoid concurrent requests to
// resolve an FQDN and then assign an address to it.
resultAny, err, _ := ns.resolveHandlerGroup.Do(fqdn, func() (any, error) {
// If we've already assigned an IP address to this fqdn, resolve to it.
if ip, ok := ns.assignedIPv4(fqdn); ok {
return dns.Result{
A: ip.asArray(),
}, nil
}
// If fqdn matches an address that should be handled by VNet, create a
// new handler for it.
handlerSpec, err := ns.tcpHandlerResolver.resolveTCPHandler(ctx, fqdn)
if err != nil {
if errors.Is(err, errNoTCPHandler) {
// Did not find any match, forward the DNS request upstream.
return dns.Result{}, nil
}
return dns.Result{}, trace.Wrap(err, "resolving TCP handler for fqdn %q", fqdn)
}
// Assign an unused IP address to this handler.
ip, err := ns.assignTCPHandler(handlerSpec, fqdn)
if err != nil {
return dns.Result{}, trace.Wrap(err, "assigning address to handler for %q", fqdn)
}
// And resolve to the assigned address.
return dns.Result{
A: ip.asArray(),
}, nil
})
if err != nil {
return dns.Result{}, trace.Wrap(err)
}
return resultAny.(dns.Result), nil
}
// ResolveAAAA implements [dns.Resolver.ResolveAAAA].
func (ns *networkStack) ResolveAAAA(ctx context.Context, fqdn string) (dns.Result, error) {
if ns.ipv6Disabled {
// IPv6 is disabled on this host, return no AAAA records.
return dns.Result{NoRecord: true}, nil
}
// Diagnostic probes return the stable IPv6 probe address — the value the diagnostic
// check compares against. No handler is allocated.
if dns.HasDiagProbePrefix(fqdn) {
return dns.Result{AAAA: ns.diagProbeIPv6}, nil
}
result, err := ns.ResolveA(ctx, fqdn)
if err != nil {
return dns.Result{}, trace.Wrap(err)
}
if result.A != ([4]byte{}) {
result.AAAA = ipv6WithSuffix(ns.ipv6Prefix, result.A[:]).As16()
result.A = [4]byte{}
}
return result, nil
}
func (ns *networkStack) assignedIPv4(fqdn string) (ipv4, bool) {
ns.state.mu.RLock()
defer ns.state.mu.RUnlock()
ipv4, ok := ns.state.assignedIPs[fqdn]
return ipv4, ok
}
func forwardBetweenTunAndNetstack(ctx context.Context, tun TUNDevice, linkEndpoint *channel.Endpoint) error {
slog.DebugContext(ctx, "Forwarding IP packets between OS and VNet.")
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return forwardNetstackToTUN(ctx, linkEndpoint, tun) })
g.Go(func() error { return forwardTUNtoNetstack(ctx, tun, linkEndpoint) })
err := g.Wait()
slog.DebugContext(ctx, "Finished forwarding IP packets between OS and VNet.")
return trace.Wrap(err)
}
func forwardNetstackToTUN(ctx context.Context, linkEndpoint *channel.Endpoint, tun TUNDevice) error {
mtu, err := tun.MTU()
if err != nil {
return trace.Wrap(err, "getting TUN device MTU")
}
bufs := [][]byte{make([]byte, device.MessageTransportHeaderSize+mtu)}
for {
packet := linkEndpoint.ReadContext(ctx)
if packet == nil {
// Nil packet is returned when context is canceled.
return trace.Wrap(ctx.Err())
}
offset := device.MessageTransportHeaderSize
views, pktOffset := packet.AsViewList()
view := views.Front()
for view != nil && pktOffset >= view.Size() {
pktOffset -= view.Size()
view = view.Next()
}
for ; view != nil; view = view.Next() {
offset += copy(bufs[0][offset:], view.AsSlice()[pktOffset:])
pktOffset = 0
}
packet.DecRef()
bufs[0] = bufs[0][:offset]
if _, err := tun.Write(bufs, device.MessageTransportHeaderSize); err != nil {
return trace.Wrap(err, "writing packets to TUN")
}
bufs[0] = bufs[0][:cap(bufs[0])]
}
}
// forwardTUNtoNetstack does not abort on ctx being canceled, but it does check the ctx error before
// returning os.ErrClosed from tun.Read.
func forwardTUNtoNetstack(ctx context.Context, tun TUNDevice, linkEndpoint *channel.Endpoint) error {
const readOffset = device.MessageTransportHeaderSize
mtu, err := tun.MTU()
if err != nil {
return trace.Wrap(err, "getting TUN device MTU")
}
bufs := make([][]byte, tun.BatchSize())
for i := range bufs {
bufs[i] = make([]byte, device.MessageTransportHeaderSize+mtu)
}
sizes := make([]int, len(bufs))
for {
n, err := tun.Read(bufs, sizes, readOffset)
if err != nil {
// tun.Read might get interrupted due to the TUN device getting closed after ctx cancellation.
if errors.Is(err, os.ErrClosed) && ctx.Err() != nil {
return ctx.Err()
}
return trace.Wrap(err, "reading packets from TUN")
}
for i := range sizes[:n] {
protocol, ok := protocolVersion(bufs[i][readOffset])
if !ok {
continue
}
packet := stack.NewPacketBuffer(stack.PacketBufferOptions{
Payload: buffer.MakeWithData(bufs[i][readOffset : readOffset+sizes[i]]),
})
linkEndpoint.InjectInbound(protocol, packet)
packet.DecRef()
}
}
}
func (ns *networkStack) addProtocolAddress(localAddress tcpip.Address) error {
protocolAddress, err := protocolAddress(localAddress)
if err != nil {
return trace.Wrap(err)
}
if err := ns.stack.AddProtocolAddress(nicID, protocolAddress, stack.AddressProperties{}); err != nil {
return trace.Errorf("%s", err)
}
return nil
}
func protocolAddress(addr tcpip.Address) (tcpip.ProtocolAddress, error) {
addrWithPrefix := addr.WithPrefix()
var protocol tcpip.NetworkProtocolNumber
switch addrWithPrefix.PrefixLen {
case 32:
protocol = ipv4network.ProtocolNumber
case 128:
protocol = ipv6network.ProtocolNumber
default:
return tcpip.ProtocolAddress{}, trace.BadParameter("unhandled prefix len %d", addrWithPrefix.PrefixLen)
}
return tcpip.ProtocolAddress{
AddressWithPrefix: addrWithPrefix,
Protocol: protocol,
}, nil
}
func protocolVersion(b byte) (tcpip.NetworkProtocolNumber, bool) {
switch b >> 4 {
case 4:
return header.IPv4ProtocolNumber, true
case 6:
return header.IPv6ProtocolNumber, true
}
return 0, false
}