Skip to content

Commit

Permalink
[v17][vnet] windows ip and route configuration
Browse files Browse the repository at this point in the history
Backport #51690 to branch/v17
  • Loading branch information
nklaassen committed Feb 6, 2025
1 parent 6980909 commit 2162204
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 2162204

Please sign in to comment.