mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-25 10:40:02 +08:00
* 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
914 lines
31 KiB
Go
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
|
|
}
|