Skip to content

Commit

Permalink
[vnet] windows ip and route configuration
Browse files Browse the repository at this point in the history
This PR adds OS configuration for VNet on Windows.
Specifically, the TUN interface is now configured with a V6 and V4 IP
address, and IP routes are configured so that IP packets in the VNet IP
ranges for each cluster are routed to the TUN interface and handled by
VNet.

This PR does *not* configure the VNet DNS nameserver on Windows, that
will come in a following PR.

With these changes, VNet kind of works, without DNS. You can manually
query the IP address of VNet's DNS server and get back a v4 and v6
address for the app. TCP connections to either of these addresses then
work for connecting to the app.
  • Loading branch information
nklaassen committed Feb 5, 2025
1 parent 0dee98d commit 4c4d5d3
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 24 deletions.
6 changes: 5 additions & 1 deletion lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import (
"golang.zx2c4.com/wireguard/tun"
)

const (
tunInterfaceName = "TeleportVNet"
)

type windowsAdminProcessConfig struct {
// clientApplicationServiceAddr is the local TCP address of the client
// application gRPC service.
Expand All @@ -50,7 +54,7 @@ func runWindowsAdminProcess(ctx context.Context, cfg *windowsAdminProcessConfig)
return trace.Wrap(err, "authenticating user process")
}

device, err := tun.CreateTUN("TeleportVNet", mtu)
device, err := tun.CreateTUN(tunInterfaceName, mtu)
if err != nil {
return trace.Wrap(err, "creating TUN device")
}
Expand Down
28 changes: 24 additions & 4 deletions lib/vnet/osconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"context"
"net"
"net/netip"
"os/exec"
"strings"
"time"

"github.com/gravitational/trace"
Expand All @@ -34,8 +36,12 @@ type osConfig struct {
dnsZones []string
}

func configureOS(ctx context.Context, osConfig *osConfig) error {
return trace.Wrap(platformConfigureOS(ctx, osConfig))
type osConfigState struct {
platformOSConfigState platformOSConfigState
}

func configureOS(ctx context.Context, osConfig *osConfig, osConfigState *osConfigState) error {
return trace.Wrap(platformConfigureOS(ctx, osConfig, &osConfigState.platformOSConfigState))
}

type targetOSConfigProvider interface {
Expand All @@ -44,6 +50,7 @@ type targetOSConfigProvider interface {

type osConfigurator struct {
targetOSConfigProvider targetOSConfigProvider
osConfigState osConfigState
}

func newOSConfigurator(targetOSConfigProvider targetOSConfigProvider) *osConfigurator {
Expand All @@ -57,15 +64,15 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error {
if err != nil {
return trace.Wrap(err)
}
if err := configureOS(ctx, desiredOSConfig); err != nil {
if err := configureOS(ctx, desiredOSConfig, &c.osConfigState); err != nil {
return trace.Wrap(err, "configuring OS")
}
return nil
}

func (c *osConfigurator) deconfigureOS(ctx context.Context) error {
// configureOS is meant to be called with an empty config to deconfigure anything necessary.
return trace.Wrap(configureOS(ctx, &osConfig{}))
return trace.Wrap(configureOS(ctx, &osConfig{}, &c.osConfigState))
}

// runOSConfigurationLoop will keep running until ctx is canceled or an
Expand Down Expand Up @@ -135,3 +142,16 @@ func tunIPv4ForCIDR(cidrRange string) (string, error) {
tunAddress[len(tunAddress)-1]++
return tunAddress.String(), nil
}

func runCommand(ctx context.Context, path string, args ...string) error {
cmdString := strings.Join(append([]string{path}, args...), " ")
log.DebugContext(ctx, "Running command", "cmd", cmdString)
cmd := exec.CommandContext(ctx, path, args...)
var output strings.Builder
cmd.Stderr = &output
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return trace.Wrap(err, `running "%s" output: %s`, cmdString, output.String())
}
return nil
}
37 changes: 22 additions & 15 deletions lib/vnet/osconfig_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,52 @@ import (
"bufio"
"context"
"os"
"os/exec"
"path/filepath"

"github.com/gravitational/trace"
)

// platformOSConfigState is not used on darwin.
type platformOSConfigState struct{}

// platformConfigureOS configures the host OS according to cfg. It is safe to
// call repeatedly, and it is meant to be called with an empty osConfig to
// deconfigure anything necessary before exiting.
func platformConfigureOS(ctx context.Context, cfg *osConfig) error {
func platformConfigureOS(ctx context.Context, cfg *osConfig, _ *platformOSConfigState) error {
// There is no need to remove IP addresses or routes, they will automatically be cleaned up when the
// process exits and the TUN is deleted.

if cfg.tunIPv4 != "" {
log.InfoContext(ctx, "Setting IPv4 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv4)
cmd := exec.CommandContext(ctx, "ifconfig", cfg.tunName, cfg.tunIPv4, cfg.tunIPv4, "up")
if err := cmd.Run(); err != nil {
return trace.Wrap(err, "running %v", cmd.Args)
log.InfoContext(ctx, "Setting IPv4 address for the TUN device.",
"device", cfg.tunName, "address", cfg.tunIPv4)
if err := runCommand(ctx,
"ifconfig", cfg.tunName, cfg.tunIPv4, cfg.tunIPv4, "up",
); err != nil {
return trace.Wrap(err)
}
}
for _, cidrRange := range cfg.cidrRanges {
log.InfoContext(ctx, "Setting an IP route for the VNet.", "netmask", cidrRange)
cmd := exec.CommandContext(ctx, "route", "add", "-net", cidrRange, "-interface", cfg.tunName)
if err := cmd.Run(); err != nil {
return trace.Wrap(err, "running %v", cmd.Args)
if err := runCommand(ctx,
"route", "add", "-net", cidrRange, "-interface", cfg.tunName,
); err != nil {
return trace.Wrap(err)
}
}

if cfg.tunIPv6 != "" {
log.InfoContext(ctx, "Setting IPv6 address for the TUN device.", "device", cfg.tunName, "address", cfg.tunIPv6)
cmd := exec.CommandContext(ctx, "ifconfig", cfg.tunName, "inet6", cfg.tunIPv6, "prefixlen", "64")
if err := cmd.Run(); err != nil {
return trace.Wrap(err, "running %v", cmd.Args)
if err := runCommand(ctx,
"ifconfig", cfg.tunName, "inet6", cfg.tunIPv6, "prefixlen", "64",
); err != nil {
return trace.Wrap(err)
}

log.InfoContext(ctx, "Setting an IPv6 route for the VNet.")
cmd = exec.CommandContext(ctx, "route", "add", "-inet6", cfg.tunIPv6, "-prefixlen", "64", "-interface", cfg.tunName)
if err := cmd.Run(); err != nil {
return trace.Wrap(err, "running %v", cmd.Args)
if err := runCommand(ctx,
"route", "add", "-inet6", cfg.tunIPv6, "-prefixlen", "64", "-interface", cfg.tunName,
); err != nil {
return trace.Wrap(err)
}
}

Expand Down
100 changes: 97 additions & 3 deletions lib/vnet/osconfig_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,108 @@ package vnet

import (
"context"
"net"
"slices"
"strconv"

"github.com/gravitational/trace"
)

// platformOSConfigState holds state about which addresses and routes have
// already been configured in the OS. Experimentally, IP routing seems to be
// flaky/broken on Windows when the same routes are repeatedly configured, as we
// currently do on MacOS. Avoid this by only configuring each IP or route once.
//
// TODO(nklaassen): it would probably be better to read the current routing
// table from the OS, compute a diff, and reconcile the routes that we need.
// This works for now but if something else overwrites our deletes our routes,
// we'll never reset them.
type platformOSConfigState struct {
configuredV4Address bool
configuredV6Address bool
configuredRanges []string

ifaceIndex string
}

func (p *platformOSConfigState) getIfaceIndex() (string, error) {
if p.ifaceIndex != "" {
return p.ifaceIndex, nil
}
iface, err := net.InterfaceByName(tunInterfaceName)
if err != nil {
return "", trace.Wrap(err, "looking up TUN interface by name %s", tunInterfaceName)
}
p.ifaceIndex = strconv.Itoa(iface.Index)
return p.ifaceIndex, nil
}

// platformConfigureOS configures the host OS according to cfg. It is safe to
// call repeatedly, and it is meant to be called with an empty osConfig to
// deconfigure anything necessary before exiting.
func platformConfigureOS(ctx context.Context, cfg *osConfig) error {
// TODO(nklaassen): implement platformConfigureOS for Windows.
return trace.NotImplemented("platformConfigureOS is not implemented on Windows")
func platformConfigureOS(ctx context.Context, cfg *osConfig, state *platformOSConfigState) error {
// There is no need to remove IP addresses or routes, they will automatically be cleaned up when the
// process exits and the TUN is deleted.

if cfg.tunIPv4 != "" {
if !state.configuredV4Address {
log.InfoContext(ctx, "Setting IPv4 address for the TUN device",
"device", cfg.tunName, "address", cfg.tunIPv4)
if err := runCommand(ctx,
"netsh", "interface", "ip", "set", "address", cfg.tunName, "static", cfg.tunIPv4,
); err != nil {
return trace.Wrap(err)
}
state.configuredV4Address = true
}
for _, cidrRange := range cfg.cidrRanges {
if slices.Contains(state.configuredRanges, cidrRange) {
continue
}
log.InfoContext(ctx, "Routing CIDR range to the TUN IP",
"device", cfg.tunName, "range", cidrRange)
ifaceIndex, err := state.getIfaceIndex()
if err != nil {
return trace.Wrap(err, "getting index for TUN interface")
}
addr, mask, err := addrMaskForCIDR(cidrRange)
if err != nil {
return trace.Wrap(err)
}
if err := runCommand(ctx,
"route", "add", addr, "mask", mask, cfg.tunIPv4, "if", ifaceIndex,
); err != nil {
return trace.Wrap(err)
}
state.configuredRanges = append(state.configuredRanges, cidrRange)
}
}

if cfg.tunIPv6 != "" && !state.configuredV6Address {
// It looks like we don't need to explicitly set a route for the IPv6
// ULA prefix, assigning the address to the interface is enough.
log.InfoContext(ctx, "Setting IPv6 address for the TUN device.",
"device", cfg.tunName, "address", cfg.tunIPv6)
if err := runCommand(ctx,
"netsh", "interface", "ipv6", "set", "address", cfg.tunName, cfg.tunIPv6,
); err != nil {
return trace.Wrap(err)
}
state.configuredV6Address = true
}

// TODO(nklaassen): configure DNS on Windows.

return nil
}

// addrMaskForCIDR returns the base address and the bitmask for a given CIDR
// range. The "route add" command wants the mask in dotted decimal format, e.g.
// for 100.64.0.0/10 the mask should be 255.192.0.0
func addrMaskForCIDR(cidr string) (string, string, error) {
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
return "", "", trace.Wrap(err, "parsing CIDR range %s", cidr)
}
return ipNet.IP.String(), net.IP(ipNet.Mask).String(), nil
}
5 changes: 4 additions & 1 deletion lib/vnet/unsupported_os.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ func runPlatformUserProcess(_ context.Context, _ *UserProcessConfig) (*ProcessMa
return nil, trace.Wrap(ErrVnetNotImplemented)
}

func platformConfigureOS(_ context.Context, _ *osConfig) error {
type platformOSConfigState struct{}

func platformConfigureOS(_ context.Context, _ *osConfig, _ *platformOSConfigState) error {
return trace.Wrap(ErrVnetNotImplemented)
}

// Satisfy unused linter.
var (
_ = newOSConfigurator
_ = (*osConfigurator).runOSConfigurationLoop
_ = runCommand
)

0 comments on commit 4c4d5d3

Please sign in to comment.