From b31c27ef4b315ac88a2bac3932c0428ff8a1558a Mon Sep 17 00:00:00 2001 From: Nic Klaassen Date: Tue, 28 Jan 2025 16:00:46 -0800 Subject: [PATCH] [vnet] run networking stack on Windows (#51280) * [vnet] run networking stack on Windows * respond to review comments --- lib/vnet/admin_process_windows.go | 80 +++++++++++++++++-- lib/vnet/client_application_service_client.go | 15 ++-- lib/vnet/dns/osnameservers_darwin.go | 6 ++ lib/vnet/dns/osnameservers_other.go | 4 +- lib/vnet/dns/osnameservers_windows.go | 37 +++++++++ lib/vnet/service_windows.go | 24 ++++-- lib/vnet/user_process_windows.go | 39 ++++++++- 7 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 lib/vnet/dns/osnameservers_windows.go diff --git a/lib/vnet/admin_process_windows.go b/lib/vnet/admin_process_windows.go index 1c30c38eb36d1..2eedec4256510 100644 --- a/lib/vnet/admin_process_windows.go +++ b/lib/vnet/admin_process_windows.go @@ -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") @@ -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 } diff --git a/lib/vnet/client_application_service_client.go b/lib/vnet/client_application_service_client.go index 7a7bdd32ad161..ebd449f336d10 100644 --- a/lib/vnet/client_application_service_client.go +++ b/lib/vnet/client_application_service_client.go @@ -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), @@ -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 } @@ -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", @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/lib/vnet/dns/osnameservers_darwin.go b/lib/vnet/dns/osnameservers_darwin.go index 4e5f22b20a2f1..35bcb425db4ed 100644 --- a/lib/vnet/dns/osnameservers_darwin.go +++ b/lib/vnet/dns/osnameservers_darwin.go @@ -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, @@ -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) } diff --git a/lib/vnet/dns/osnameservers_other.go b/lib/vnet/dns/osnameservers_other.go index a353020fa0b67..06cca8d0856d3 100644 --- a/lib/vnet/dns/osnameservers_other.go +++ b/lib/vnet/dns/osnameservers_other.go @@ -14,8 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows package dns diff --git a/lib/vnet/dns/osnameservers_windows.go b/lib/vnet/dns/osnameservers_windows.go new file mode 100644 index 0000000000000..5ca17e61735a9 --- /dev/null +++ b/lib/vnet/dns/osnameservers_windows.go @@ -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 . + +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 +} diff --git a/lib/vnet/service_windows.go b/lib/vnet/service_windows.go index 1387d6cca4407..6bf09342c9489 100644 --- a/lib/vnet/service_windows.go +++ b/lib/vnet/service_windows.go @@ -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" @@ -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) } @@ -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 { @@ -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 @@ -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 diff --git a/lib/vnet/user_process_windows.go b/lib/vnet/user_process_windows.go index 9fccd0bb528b3..0eb3dcf97762e 100644 --- a/lib/vnet/user_process_windows.go +++ b/lib/vnet/user_process_windows.go @@ -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 @@ -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 }