Skip to content

Commit

Permalink
[vnet] run networking stack on Windows (#51280)
Browse files Browse the repository at this point in the history
* [vnet] run networking stack on Windows

* respond to review comments
  • Loading branch information
nklaassen authored Jan 29, 2025
1 parent e936ab4 commit b31c27e
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 22 deletions.
80 changes: 75 additions & 5 deletions lib/vnet/admin_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,36 @@ package vnet

import (
"context"
"time"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"golang.zx2c4.com/wireguard/tun"
)

type windowsAdminProcessConfig struct {
// clientApplicationServiceAddr is the local TCP address of the client
// application gRPC service.
clientApplicationServiceAddr string
}

// runWindowsAdminProcess must run as administrator. It creates and sets up a TUN
// device, runs the VNet networking stack, and handles OS configuration. It will
// continue to run until [ctx] is canceled or encountering an unrecoverable
// error.
func runWindowsAdminProcess(ctx context.Context) error {
func runWindowsAdminProcess(ctx context.Context, cfg *windowsAdminProcessConfig) error {
log.InfoContext(ctx, "Running VNet admin process")

clt, err := newClientApplicationServiceClient(ctx, cfg.clientApplicationServiceAddr)
if err != nil {
return trace.Wrap(err, "creating user process client")
}
defer clt.close()

if err := authenticateUserProcess(ctx, clt); err != nil {
return trace.Wrap(err, "authenticating user process")
}

device, err := tun.CreateTUN("TeleportVNet", mtu)
if err != nil {
return trace.Wrap(err, "creating TUN device")
Expand All @@ -38,8 +58,58 @@ func runWindowsAdminProcess(ctx context.Context) error {
return trace.Wrap(err, "getting TUN device name")
}
log.InfoContext(ctx, "Created TUN interface", "tun", tunName)
// TODO(nklaassen): actually run VNet. For now, just stay alive until the
// context is canceled.
<-ctx.Done()
return trace.Wrap(ctx.Err())

networkStackConfig, err := newWindowsNetworkStackConfig(device, clt)
if err != nil {
return trace.Wrap(err, "creating network stack config")
}
networkStack, err := newNetworkStack(networkStackConfig)
if err != nil {
return trace.Wrap(err, "creating network stack")
}

ctx, cancel := context.WithCancel(ctx)
defer cancel()
errCh := make(chan error)
go func() {
errCh <- trace.Wrap(networkStack.run(ctx), "running network stack")
}()
loop:
for {
select {
case <-time.After(time.Second):
if err := clt.Ping(ctx); err != nil {
log.InfoContext(ctx, "Failed to ping client application, it may have exited, shutting down", "error", err)
break loop
}
case <-ctx.Done():
log.InfoContext(ctx, "Context canceled, shutting down", "error", err)
break loop
}
}
// Cancel the context and wait for networkStack.run to terminate.
cancel()
err = <-errCh
return trace.Wrap(err, "running VNet network stack")
}

func newWindowsNetworkStackConfig(tun tunDevice, clt *clientApplicationServiceClient) (*networkStackConfig, error) {
appProvider := newRemoteAppProvider(clt)
appResolver := newTCPAppResolver(appProvider, clockwork.NewRealClock())
ipv6Prefix, err := NewIPv6Prefix()
if err != nil {
return nil, trace.Wrap(err, "creating new IPv6 prefix")
}
dnsIPv6 := ipv6WithSuffix(ipv6Prefix, []byte{2})
return &networkStackConfig{
tunDevice: tun,
ipv6Prefix: ipv6Prefix,
dnsIPv6: dnsIPv6,
tcpHandlerResolver: appResolver,
}, nil
}

func authenticateUserProcess(ctx context.Context, clt *clientApplicationServiceClient) error {
// TODO(nklaassen): implement process authentication.
return nil
}
15 changes: 8 additions & 7 deletions lib/vnet/client_application_service_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type clientApplicationServiceClient struct {
}

func newClientApplicationServiceClient(ctx context.Context, addr string) (*clientApplicationServiceClient, error) {
// TODO(nklaassen): add mTLS credentials for client application service.
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(interceptors.GRPCClientUnaryErrorInterceptor),
Expand All @@ -58,7 +59,7 @@ func (c *clientApplicationServiceClient) close() error {
// Ping pings the client application.
func (c *clientApplicationServiceClient) Ping(ctx context.Context) error {
if _, err := c.clt.Ping(ctx, &vnetv1.PingRequest{}); err != nil {
return trace.Wrap(err, "pinging client application")
return trace.Wrap(err, "calling Ping rpc")
}
return nil
}
Expand All @@ -70,7 +71,7 @@ func (c *clientApplicationServiceClient) AuthenticateProcess(ctx context.Context
PipePath: pipePath,
})
if err != nil {
return trace.Wrap(err, "authenticating process")
return trace.Wrap(err, "calling AuthenticateProcess rpc")
}
if resp.Version != api.Version {
return trace.BadParameter("version mismatch, user process version is %s, admin process version is %s",
Expand All @@ -86,7 +87,7 @@ func (c *clientApplicationServiceClient) ResolveAppInfo(ctx context.Context, fqd
Fqdn: fqdn,
})
if err != nil {
return nil, trace.Wrap(err, "resolving app info")
return nil, trace.Wrap(err, "calling ResolveAppInfo rpc")
}
return resp.GetAppInfo(), nil
}
Expand All @@ -98,7 +99,7 @@ func (c *clientApplicationServiceClient) ReissueAppCert(ctx context.Context, app
TargetPort: uint32(targetPort),
})
if err != nil {
return nil, trace.Wrap(err, "reissuing app cert")
return nil, trace.Wrap(err, "calling ReissueAppCert rpc")
}
return resp.GetCert(), nil
}
Expand All @@ -108,7 +109,7 @@ func (c *clientApplicationServiceClient) ReissueAppCert(ctx context.Context, app
func (c *clientApplicationServiceClient) SignForApp(ctx context.Context, req *vnetv1.SignForAppRequest) ([]byte, error) {
resp, err := c.clt.SignForApp(ctx, req)
if err != nil {
return nil, trace.Wrap(err, "signing for app")
return nil, trace.Wrap(err, "calling SignForApp rpc")
}
return resp.GetSignature(), nil
}
Expand All @@ -119,7 +120,7 @@ func (c *clientApplicationServiceClient) OnNewConnection(ctx context.Context, ap
AppKey: appKey,
})
if err != nil {
return trace.Wrap(err)
return trace.Wrap(err, "calling OnNewConnection rpc")
}
return nil
}
Expand All @@ -132,7 +133,7 @@ func (c *clientApplicationServiceClient) OnInvalidLocalPort(ctx context.Context,
TargetPort: uint32(targetPort),
})
if err != nil {
return trace.Wrap(err)
return trace.Wrap(err, "calling OnInvalidLocalPort rpc")
}
return nil
}
6 changes: 6 additions & 0 deletions lib/vnet/dns/osnameservers_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,14 @@ const (
confFilePath = "/etc/resolv.conf"
)

// OSUpstreamNameserverSource provides the list of upstream DNS nameservers
// configured in the OS. The VNet DNS resolver will forward unhandles queries to
// these nameservers.
type OSUpstreamNameserverSource struct {
ttlCache *utils.FnCache
}

// NewOSUpstreamNameserverSource returns a new *OSUpstreamNameserverSource.
func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) {
ttlCache, err := utils.NewFnCache(utils.FnCacheConfig{
TTL: 10 * time.Second,
Expand All @@ -50,6 +54,8 @@ func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) {
}, nil
}

// UpstreamNameservers returns a cached view of the host OS's current default
// nameservers, as found in /etc/resolv.conf.
func (s *OSUpstreamNameserverSource) UpstreamNameservers(ctx context.Context) ([]string, error) {
return utils.FnCacheGet(ctx, s.ttlCache, 0, s.upstreamNameservers)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/vnet/dns/osnameservers_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
// 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/>.

//go:build !darwin
// +build !darwin
//go:build !darwin && !windows
// +build !darwin,!windows

package dns

Expand Down
37 changes: 37 additions & 0 deletions lib/vnet/dns/osnameservers_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Teleport
// Copyright (C) 2025 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 dns

import "context"

// OSUpstreamNameserverSource provides the list of upstream DNS nameservers
// configured in the OS. The VNet DNS resolver will forward unhandles queries to
// these nameservers.
type OSUpstreamNameserverSource struct{}

// NewOSUpstreamNameserverSource returns a new *OSUpstreamNameserverSource.
func NewOSUpstreamNameserverSource() (*OSUpstreamNameserverSource, error) {
return &OSUpstreamNameserverSource{}, nil
}

// UpstreamNameservers is net yet implemented and currently returns a nil/empty
// list of upstream nameservers. It does not return an error so that the
// networking stack can actually run without just immediately exiting.
func (s *OSUpstreamNameserverSource) UpstreamNameservers(ctx context.Context) ([]string, error) {
// TODO(nklaassen): implement UpstreamNameservers on windows.
return nil, nil
}
24 changes: 19 additions & 5 deletions lib/vnet/service_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"syscall"
"time"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
Expand All @@ -40,8 +41,8 @@ const (
// runService is called from the normal user process to run the VNet Windows in
// the background and wait for it to exit. It will terminate the service and
// return immediately if [ctx] is canceled.
func runService(ctx context.Context) error {
service, err := startService(ctx)
func runService(ctx context.Context, cfg *windowsAdminProcessConfig) error {
service, err := startService(ctx, cfg)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -69,7 +70,7 @@ func runService(ctx context.Context) error {
}

// startService starts the Windows VNet admin service in the background.
func startService(ctx context.Context) (*mgr.Service, error) {
func startService(ctx context.Context, cfg *windowsAdminProcessConfig) (*mgr.Service, error) {
// Avoid [mgr.Connect] because it requests elevated permissions.
scManager, err := windows.OpenSCManager(nil /*machine*/, nil /*database*/, windows.SC_MANAGER_CONNECT)
if err != nil {
Expand All @@ -88,7 +89,7 @@ func startService(ctx context.Context) (*mgr.Service, error) {
Name: serviceName,
Handle: serviceHandle,
}
if err := service.Start(ServiceCommand); err != nil {
if err := service.Start(ServiceCommand, "--addr", cfg.clientApplicationServiceAddr); err != nil {
return nil, trace.Wrap(err, "starting Windows service %s", serviceName)
}
return service, nil
Expand Down Expand Up @@ -157,7 +158,20 @@ loop:
}

func (s *windowsService) run(ctx context.Context, args []string) error {
if err := runWindowsAdminProcess(ctx); err != nil {
var clientApplicationServiceAddr string
app := kingpin.New(serviceName, "Teleport VNet Windows Service")
serviceCmd := app.Command("vnet-service", "Start the VNet service.")
serviceCmd.Flag("addr", "client application service address").Required().StringVar(&clientApplicationServiceAddr)
cmd, err := app.Parse(args[1:])
if err != nil {
return trace.Wrap(err, "parsing runtime arguments to Windows service")
}
if cmd != serviceCmd.FullCommand() {
return trace.BadParameter("Windows service runtime arguments did not match \"vnet-service\", args: %v", args[1:])
}
if err := runWindowsAdminProcess(ctx, &windowsAdminProcessConfig{
clientApplicationServiceAddr: clientApplicationServiceAddr,
}); err != nil {
return trace.Wrap(err, "running admin process")
}
return nil
Expand Down
39 changes: 36 additions & 3 deletions lib/vnet/user_process_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,15 @@ package vnet

import (
"context"
"net"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/gravitational/teleport/api/utils/grpc/interceptors"
vnetv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/v1"
)

// runPlatformUserProcess launches a Windows service in the background that will
Expand All @@ -35,10 +42,36 @@ func runPlatformUserProcess(ctx context.Context, config *UserProcessConfig) (pm
}
}()

listener, err := net.Listen("tcp", ":0")
if err != nil {
return nil, trace.Wrap(err, "listening on tcp socket")
}
pm, processCtx := newProcessManager()
pm.AddCriticalBackgroundTask("VNet Windows service", func() error {
return trace.Wrap(runService(processCtx), "running VNet Windows service in the background")
pm.AddCriticalBackgroundTask("tcp socket closer", func() error {
<-processCtx.Done()
return trace.Wrap(listener.Close())
})
pm.AddCriticalBackgroundTask("admin process", func() error {
return trace.Wrap(runService(processCtx, &windowsAdminProcessConfig{
clientApplicationServiceAddr: listener.Addr().String(),
}))
})
pm.AddCriticalBackgroundTask("gRPC service", func() error {
log.InfoContext(processCtx, "Starting gRPC service", "addr", listener.Addr().String())
// TODO(nklaassen): add mTLS credentials for client application service.
grpcServer := grpc.NewServer(
grpc.Creds(insecure.NewCredentials()),
grpc.UnaryInterceptor(interceptors.GRPCServerUnaryErrorInterceptor),
grpc.StreamInterceptor(interceptors.GRPCServerStreamErrorInterceptor),
)
clock := clockwork.NewRealClock()
appProvider := newLocalAppProvider(config.ClientApplication, clock)
svc := newClientApplicationService(appProvider)
vnetv1.RegisterClientApplicationServiceServer(grpcServer, svc)
if err := grpcServer.Serve(listener); err != nil {
return trace.Wrap(err, "serving VNet user process gRPC service")
}
return nil
})
// TODO(nklaassen): run user process gRPC service.
return pm, nil
}

0 comments on commit b31c27e

Please sign in to comment.