Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[vnet] windows ip and route configuration #51690

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we can reuse the service name defined in https://github.com/gravitational/teleport/blob/master/lib/vnet/service_windows.go#L36.
If we want a different name for the tun, maybe spell out TeleportVNetTUN

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i'll leave it as-is they are separate concepts that just happen to be the same string, i prefer naming the TUN interface just 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
)
Loading