diff --git a/api/client/client.go b/api/client/client.go index 74022058526..65a34440b9a 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -870,6 +870,11 @@ func (c *Client) VnetConfigServiceClient() vnet.VnetConfigServiceClient { return vnet.NewVnetConfigServiceClient(c.conn) } +// GetVnetConfig returns the singleton VnetConfig resource. +func (c *Client) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) { + return c.VnetConfigServiceClient().GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{}) +} + // Ping gets basic info about the auth server. func (c *Client) Ping(ctx context.Context) (proto.PingResponse, error) { rsp, err := c.grpc.Ping(ctx, &proto.PingRequest{}) diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go index 055fe08d3c9..2b495ca5e2f 100644 --- a/lib/auth/authclient/clt.go +++ b/lib/auth/authclient/clt.go @@ -1430,6 +1430,7 @@ type ClientI interface { services.Integrations services.KubeWaitingContainer services.Notifications + services.VnetConfigGetter types.Events types.WebSessionsGetter diff --git a/lib/services/vnet_config.go b/lib/services/vnet_config.go index 02f4738f488..6c967438c76 100644 --- a/lib/services/vnet_config.go +++ b/lib/services/vnet_config.go @@ -22,10 +22,15 @@ import ( "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" ) -// VnetConfigService is an interface for the VnetConfig service. -type VnetConfigService interface { +// VnetConfigGetter is an interface for getting the cluster singleton VnetConfig. +type VnetConfigGetter interface { // GetVnetConfig returns the singleton VnetConfig resource. GetVnetConfig(context.Context) (*vnet.VnetConfig, error) +} + +// VnetConfigService is an interface for the VnetConfig service. +type VnetConfigService interface { + VnetConfigGetter // CreateVnetConfig does basic validation and creates a VnetConfig resource. CreateVnetConfig(ctx context.Context, vnetConfig *vnet.VnetConfig) (*vnet.VnetConfig, error) diff --git a/lib/teleterm/vnet/service.go b/lib/teleterm/vnet/service.go index df45207ff48..d94c15247db 100644 --- a/lib/teleterm/vnet/service.go +++ b/lib/teleterm/vnet/service.go @@ -28,7 +28,6 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport" - vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" "github.com/gravitational/teleport/api/types" prehogv1alpha "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha" apiteleterm "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" @@ -360,16 +359,6 @@ func (p *appProvider) GetDialOptions(ctx context.Context, profileName string) (* return dialOpts, nil } -func (p *appProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) { - clusterClient, err := p.getCachedClient(ctx, profileName, leafClusterName) - if err != nil { - return nil, trace.Wrap(err) - } - vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient() - vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{}) - return vnetConfig, trace.Wrap(err) -} - // OnNewConnection submits a usage event once per appProvider lifetime. // That is, if a user makes multiple connections to a single app, OnNewConnection submits a single // event. This is to mimic how Connect submits events for its app gateways. This lets us compare diff --git a/lib/vnet/app_resolver.go b/lib/vnet/app_resolver.go index b4a634e3c26..c6ab2befd2e 100644 --- a/lib/vnet/app_resolver.go +++ b/lib/vnet/app_resolver.go @@ -21,6 +21,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "log/slog" "net" @@ -33,7 +34,6 @@ import ( "github.com/gravitational/teleport" apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" @@ -61,9 +61,6 @@ type AppProvider interface { // GetDialOptions returns ALPN dial options for the profile. GetDialOptions(ctx context.Context, profileName string) (*DialOptions, error) - // GetVnetConfig returns the cluster VnetConfig resource. - GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) - // OnNewConnection gets called whenever a new connection is about to be established through VNet. // By the time OnNewConnection, VNet has already verified that the user holds a valid cert for the // app. @@ -120,7 +117,7 @@ func NewTCPAppResolver(appProvider AppProvider, opts ...tcpAppResolverOption) (* opt(r) } r.clock = cmp.Or(r.clock, clockwork.NewRealClock()) - r.clusterConfigCache = newClusterConfigCache(appProvider.GetVnetConfig, r.clock) + r.clusterConfigCache = newClusterConfigCache(appProvider.GetCachedClient, r.clock) r.customDNSZoneChecker = newCustomDNSZoneValidator(r.lookupTXT) return r, nil } @@ -155,63 +152,107 @@ func (r *TCPAppResolver) ResolveTCPHandler(ctx context.Context, fqdn string) (*T // This is a query for the proxy address, which we'll never want to handle. return nil, ErrNoTCPHandler } - if match, err := r.fqdnMatchesProfile(ctx, profileName, fqdn); err != nil { - return nil, trace.Wrap(err) - } else if !match { - continue - } - slog := r.slog.With("profile", profileName, "fqdn", fqdn) - rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "") + clusterClient, err := r.clusterClientForAppFQDN(ctx, profileName, fqdn) if err != nil { - // The user might be logged out from this one cluster (and retryWithRelogin isn't working). Don't - // return an error so that DNS resolution will be forwarded upstream instead of failing, to avoid - // breaking e.g. web app access (we don't know if this is a web or TCP app yet because we can't - // log in). + if errors.Is(err, errNoMatch) { + continue + } + // The user might be logged out from this one cluster (and retryWithRelogin isn't working). Log + // the error but don't return it so that DNS resolution will be forwarded upstream instead of + // failing, to avoid breaking e.g. web app access (we don't know if this is a web or TCP app yet + // because we can't log in). slog.ErrorContext(ctx, "Failed to get teleport client.", "error", err) continue } - return r.resolveTCPHandlerForCluster(ctx, slog, rootClient.CurrentCluster(), profileName, "", fqdn) + + leafClusterName := "" + if clusterClient.ClusterName() != profileName { + leafClusterName = clusterClient.ClusterName() + } + + slog := r.slog.With("profile", profileName, "fqdn", fqdn, "leaf_cluster", leafClusterName) + return r.resolveTCPHandlerForCluster(ctx, slog, clusterClient, profileName, leafClusterName, fqdn) } // fqdn did not match any profile, forward the request upstream. return nil, ErrNoTCPHandler } -func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fqdn string) (bool, error) { - if isSubdomain(fqdn, profileName) { - // The queried app fqdn is a subdomain of the proxy address, this is a match. - return true, nil - } - // Not a proxy address subdomain, must check custom DNS zones. +var errNoMatch = errors.New("cluster does not match queried FQDN") - // TODO(nklaassen): support leaf clusters. - vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClustername*/) - if err != nil { - // Good chance we're here because the user is not logged in to the profile. - r.slog.ErrorContext(ctx, "Failed to get VnetConfig, not checking custom DNS zones.", "profile", profileName, "error", err) - return false, nil - } - - // TODO(nklaassen): support leaf clusters. +func (r *TCPAppResolver) clusterClientForAppFQDN(ctx context.Context, profileName, fqdn string) (ClusterClient, error) { rootClient, err := r.appProvider.GetCachedClient(ctx, profileName, "") if err != nil { - r.slog.ErrorContext(ctx, "Failed to get teleport client, not checking custom DNS zones.", "profile", profileName, "error", err) - return false, nil + r.slog.ErrorContext(ctx, "Failed to get root cluster client, apps in this cluster will not be resolved.", "profile", profileName, "error", err) + return nil, errNoMatch } - clusterName := rootClient.ClusterName() - for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() { - if !isSubdomain(fqdn, zone.GetSuffix()) { - // The queried app fqdn is not a subdomain of this custom zone suffix, skip it. + + if isSubdomain(fqdn, profileName) { + // The queried app fqdn is direct subdomain of this cluster proxy address. + return rootClient, nil + } + + leafClusters, err := getLeafClusters(ctx, rootClient) + if err != nil { + // Good chance we're here because the user is not logged in to the profile. + r.slog.ErrorContext(ctx, "Failed to list leaf clusters, apps in this cluster will not be resolved.", "profile", profileName, "error", err) + return nil, errNoMatch + } + + // Prefix with an empty string to represent the root cluster. + allClusters := append([]string{""}, leafClusters...) + for _, leafClusterName := range allClusters { + clusterClient, err := r.appProvider.GetCachedClient(ctx, profileName, leafClusterName) + if err != nil { + r.slog.ErrorContext(ctx, "Failed to get cluster client, apps in this cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err) continue } - // The queried app fqdn is a subdomain of this custom zone suffix. Check if the custom zone is valid. - if err := r.customDNSZoneChecker.validate(ctx, clusterName, zone.GetSuffix()); err != nil { - r.slog.ErrorContext(ctx, "Failed to validate custom DNS zone %q for cluster %q", "error", err) - return false, trace.Wrap(err, "validating custom DNS zone") + + clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient) + if err != nil { + r.slog.ErrorContext(ctx, "Failed to get VnetConfig, apps in the cluster will not be resolved.", "profile", profileName, "leaf_cluster", leafClusterName, "error", err) + continue + } + for _, zone := range clusterConfig.dnsZones { + if !isSubdomain(fqdn, zone) { + // The queried app fqdn is not a subdomain of this zone, skip it. + continue + } + + // Found a matching cluster. + + if zone == clusterConfig.proxyPublicAddr { + // We don't need to validate a custom DNS zone if this is the proxy public address, this is a + // normal app public_addr. + return clusterClient, nil + } + // The queried app fqdn is a subdomain of this custom zone. Check if the zone is valid. + if err := r.customDNSZoneChecker.validate(ctx, clusterConfig.clusterName, zone); err != nil { + // Return an error here since the FQDN does match this custom zone, but the zone failed to + // validate. + return nil, trace.Wrap(err, "validating custom DNS zone %q matching queried FQDN %q", zone, fqdn) + } + return clusterClient, nil + } + } + return nil, errNoMatch +} + +func getLeafClusters(ctx context.Context, rootClient ClusterClient) ([]string, error) { + var leafClusters []string + nextPage := "" + for { + remoteClusters, nextPage, err := rootClient.CurrentCluster().ListRemoteClusters(ctx, 0, nextPage) + if err != nil { + return nil, trace.Wrap(err) + } + for _, rc := range remoteClusters { + leafClusters = append(leafClusters, rc.GetName()) + } + if nextPage == "" { + return leafClusters, nil } - return true, nil } - return false, nil } // resolveTCPHandlerForCluster takes a cluster client and resolves [fqdn] to a [TCPHandlerSpec] if a matching @@ -221,13 +262,13 @@ func (r *TCPAppResolver) fqdnMatchesProfile(ctx context.Context, profileName, fq func (r *TCPAppResolver) resolveTCPHandlerForCluster( ctx context.Context, slog *slog.Logger, - clt apiclient.GetResourcesClient, + clusterClient ClusterClient, profileName, leafClusterName, fqdn string, ) (*TCPHandlerSpec, error) { // An app public_addr could technically be full-qualified or not, match either way. expr := fmt.Sprintf(`(resource.spec.public_addr == "%s" || resource.spec.public_addr == "%s") && hasPrefix(resource.spec.uri, "tcp://")`, strings.TrimSuffix(fqdn, "."), fqdn) - resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clt, &proto.ListResourcesRequest{ + resp, err := apiclient.GetResourcePage[types.AppServer](ctx, clusterClient.CurrentCluster(), &proto.ListResourcesRequest{ ResourceType: types.KindAppServer, PredicateExpression: expr, Limit: 1, @@ -248,19 +289,13 @@ func (r *TCPAppResolver) resolveTCPHandlerForCluster( return nil, trace.Wrap(err) } - var cidrRange string - vnetConfig, err := r.clusterConfigCache.getVnetConfig(ctx, profileName, leafClusterName) - switch { - case err == nil: - cidrRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange) - case trace.IsNotFound(err) || trace.IsNotImplemented(err): - cidrRange = defaultIPv4CIDRRange - default: + clusterConfig, err := r.clusterConfigCache.getClusterConfig(ctx, clusterClient) + if err != nil { return nil, trace.Wrap(err) } return &TCPHandlerSpec{ - IPv4CIDRRange: cidrRange, + IPv4CIDRRange: clusterConfig.ipv4CIDRRange, TCPHandler: appHandler, }, nil } diff --git a/lib/vnet/clusterconfigcache.go b/lib/vnet/clusterconfigcache.go index 88efe74612c..fcba8bd034f 100644 --- a/lib/vnet/clusterconfigcache.go +++ b/lib/vnet/clusterconfigcache.go @@ -17,25 +17,38 @@ package vnet import ( + "cmp" "context" + "net" + "strings" "sync" "time" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "golang.org/x/sync/singleflight" - - "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" ) -type getClusterConfigFunc = func(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) +type getClusterClientFunc = func(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error) -type cacheEntry struct { - vnetConfig *vnet.VnetConfig - expires time.Time +type clusterConfig struct { + // clusterName is the name of the cluster as reported by Ping. + clusterName string + // proxyPublicAddr is the public address of the proxy as reported by Ping, with any ports removed, this is + // just the hostname. This is often but not always identical to the clusterName. For root clusters this + // will be the same as the profile name (the profile is named after the proxy public addr). + proxyPublicAddr string + // dnsZones is the list of DNS zones that are valid for this cluster, this includes proxyPublicAddr *and* + // any configured custom DNS zones for the cluster. + dnsZones []string + // ipv4CIDRRange is the CIDR range that IPv4 addresses should be assigned from for apps in this cluster. + ipv4CIDRRange string + // expires is the time at which this information should be considered stale and refetched. Stale data may + // be used if a subsequent fetch fails. + expires time.Time } -func (e *cacheEntry) stale(clock clockwork.Clock) bool { +func (e *clusterConfig) stale(clock clockwork.Clock) bool { return clock.Now().After(e.expires) } @@ -48,66 +61,90 @@ func (e *cacheEntry) stale(clock clockwork.Clock) bool { type clusterConfigCache struct { flightGroup singleflight.Group clock clockwork.Clock - get getClusterConfigFunc - cache map[string]cacheEntry + getClient getClusterClientFunc + cache map[string]*clusterConfig mu sync.RWMutex } -func newClusterConfigCache(get getClusterConfigFunc, clock clockwork.Clock) *clusterConfigCache { +func newClusterConfigCache(getClient getClusterClientFunc, clock clockwork.Clock) *clusterConfigCache { return &clusterConfigCache{ - clock: clock, - get: get, - cache: make(map[string]cacheEntry), + clock: clock, + getClient: getClient, + cache: make(map[string]*clusterConfig), } } -func (c *clusterConfigCache) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) { - k := clusterCacheKey(profileName, leafClusterName) +func (c *clusterConfigCache) getClusterConfig(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) { + k := clusterClient.ClusterName() // Use a singleflight.Group to avoid concurrent requests for the same cluster VnetConfig. - vnetConfig, err, _ := c.flightGroup.Do(k, func() (any, error) { + result, err, _ := c.flightGroup.Do(k, func() (any, error) { // Check the cache inside flightGroup.Do to avoid the chance of immediate repeat calls to the cluster. c.mu.RLock() existingCacheEntry, existingCacheEntryFound := c.cache[k] c.mu.RUnlock() if existingCacheEntryFound && !existingCacheEntry.stale(c.clock) { - return existingCacheEntry.vnetConfig, nil + return existingCacheEntry, nil } - vnetConfig, err := c.get(ctx, profileName, leafClusterName) - if trace.IsNotFound(err) || trace.IsNotImplemented(err) { - // Default to the empty config on NotFound or NotImplemented. - vnetConfig = &vnet.VnetConfig{} - err = nil - } + clusterConfig, err := c.getClusterConfigUncached(ctx, clusterClient) if err != nil { // It's better to return a stale cached VnetConfig than an error. The profile probably expired and // we want to keep functioning until a relogin. We don't expect the VnetConfig to change very // often. if existingCacheEntryFound { - return existingCacheEntry.vnetConfig, nil + return existingCacheEntry, nil } return nil, trace.Wrap(err) } c.mu.Lock() - c.cache[k] = cacheEntry{ - vnetConfig: vnetConfig, - expires: c.clock.Now().Add(5 * time.Minute), - } + c.cache[k] = clusterConfig c.mu.Unlock() - return vnetConfig, nil + return clusterConfig, nil }) if err != nil { return nil, trace.Wrap(err) } - return vnetConfig.(*vnet.VnetConfig), nil + return result.(*clusterConfig), nil } -func clusterCacheKey(profileName, leafClusterName string) string { - if leafClusterName != "" { - return profileName + "/" + leafClusterName +func (c *clusterConfigCache) getClusterConfigUncached(ctx context.Context, clusterClient ClusterClient) (*clusterConfig, error) { + pingResp, err := clusterClient.CurrentCluster().Ping(ctx) + if err != nil { + return nil, trace.Wrap(err) } - return profileName + + clusterName := pingResp.ClusterName + proxyPublicAddr := pingResp.ProxyPublicAddr + if strings.Contains(proxyPublicAddr, ":") { + proxyPublicAddr, _, err = net.SplitHostPort(pingResp.ProxyPublicAddr) + if err != nil { + return nil, trace.Wrap(err, "parsing proxy public addr") + } + } + + dnsZones := []string{proxyPublicAddr} + ipv4CIDRRange := defaultIPv4CIDRRange + + vnetConfig, err := clusterClient.CurrentCluster().GetVnetConfig(ctx) + if trace.IsNotFound(err) || trace.IsNotImplemented(err) { + // Use the defaults set above, nothing to do here. + } else if err != nil { + return nil, trace.Wrap(err) + } else { + for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() { + dnsZones = append(dnsZones, zone.GetSuffix()) + } + ipv4CIDRRange = cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange) + } + + return &clusterConfig{ + clusterName: clusterName, + proxyPublicAddr: proxyPublicAddr, + dnsZones: dnsZones, + ipv4CIDRRange: ipv4CIDRRange, + expires: c.clock.Now().Add(5 * time.Minute), + }, nil } diff --git a/lib/vnet/osconfig.go b/lib/vnet/osconfig.go index 1ef9a91a7df..0d77606f23e 100644 --- a/lib/vnet/osconfig.go +++ b/lib/vnet/osconfig.go @@ -17,17 +17,14 @@ package vnet import ( - "cmp" "context" "log/slog" "net" "os" - "strings" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" - "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils" @@ -71,7 +68,7 @@ func newOSConfigurator(tunName, ipv6Prefix, dnsAddr string) (*osConfigurator, er homePath: homePath, clientStore: client.NewFSClientStore(homePath), } - configurator.clusterConfigCache = newClusterConfigCache(configurator.getVnetConfig, clockwork.NewRealClock()) + configurator.clusterConfigCache = newClusterConfigCache(configurator.getClusterClient, clockwork.NewRealClock()) return configurator, nil } @@ -85,8 +82,14 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { return trace.Wrap(err, "listing user profiles") } for _, profileName := range profileNames { - // TODO(nklaassen): support leaf clusters - vnetConfig, err := c.clusterConfigCache.getVnetConfig(ctx, profileName, "" /*leafClusterName*/) + rootClient, err := c.getClusterClient(ctx, profileName, "" /*leafClusterName*/) + if err != nil { + slog.WarnContext(ctx, + "Failed to create root cluster client, profile may be expired, not configuring VNet for this cluster", + "profile", profileName, "error", err) + continue + } + clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, rootClient) if err != nil { slog.WarnContext(ctx, "Failed to load VNet configuration, profile may be expired, not configuring VNet for this cluster", @@ -94,18 +97,36 @@ func (c *osConfigurator) updateOSConfiguration(ctx context.Context) error { continue } - // profileName is the web proxy address, add the default DNS zone for it. - // TODO(nklaassen): add the custom DNS zones as well, after the rest of VNet supports it. - dnsZones = append(dnsZones, profileName) - for _, zone := range vnetConfig.GetSpec().GetCustomDnsZones() { - suffix := zone.GetSuffix() - // Trim any leading or trailing "." to match expected format. - zone := strings.TrimPrefix(strings.TrimSuffix(suffix, "."), ".") - dnsZones = append(dnsZones, zone) - } + dnsZones = append(dnsZones, clusterConfig.dnsZones...) + cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange) - cidrRange := cmp.Or(vnetConfig.GetSpec().GetIpv4CidrRange(), defaultIPv4CIDRRange) - cidrRanges = append(cidrRanges, cidrRange) + leafClusters, err := getLeafClusters(ctx, rootClient) + if err != nil { + slog.WarnContext(ctx, + "Failed to list leaf clusters, profile may be expired, not configuring VNet for this cluster", + "profile", profileName, "error", err) + continue + } + for _, leafClusterName := range leafClusters { + clusterClient, err := c.getClusterClient(ctx, profileName, leafClusterName) + if err != nil { + slog.WarnContext(ctx, + "Failed to create leaf cluster client, not configuring VNet for this cluster", + "profile", profileName, "leaf_cluster", leafClusterName, "error", err) + continue + } + + clusterConfig, err := c.clusterConfigCache.getClusterConfig(ctx, clusterClient) + if err != nil { + slog.WarnContext(ctx, + "Failed to load VNet configuration, not configuring VNet for this cluster", + "profile", profileName, "leaf_cluster", leafClusterName, "error", err) + continue + } + + dnsZones = append(dnsZones, clusterConfig.dnsZones...) + cidrRanges = append(cidrRanges, clusterConfig.ipv4CIDRRange) + } } dnsZones = utils.Deduplicate(dnsZones) @@ -153,17 +174,7 @@ func (c *osConfigurator) setTunIPv4FromCIDR(cidrRange string) error { return nil } -func (c *osConfigurator) getVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnet.VnetConfig, error) { - clt, err := c.vnetConfigClient(ctx, profileName, leafClusterName) - if err != nil { - return nil, trace.Wrap(err, "getting vnet client for profile %s %s", profileName, leafClusterName) - } - - vnetConfig, err := clt.GetVnetConfig(ctx, &vnet.GetVnetConfigRequest{}) - return vnetConfig, trace.Wrap(err) -} - -func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leafClusterName string) (vnet.VnetConfigServiceClient, error) { +func (c *osConfigurator) getClusterClient(ctx context.Context, profileName, leafClusterName string) (ClusterClient, error) { // This runs in the root process, so obviously we don't have access to the client cache in the user // process. This loads cluster profiles and credentials from TELEPORT_HOME. clientConfig := &client.Config{ @@ -180,11 +191,10 @@ func (c *osConfigurator) vnetConfigClient(ctx context.Context, profileName, leaf return nil, trace.Wrap(err, "creating new teleport client") } - clusterClt, err := tc.ConnectToCluster(ctx) + clt, err := tc.ConnectToCluster(ctx) if err != nil { return nil, trace.Wrap(err, "connecting to cluster") } - vnetConfigClt := clusterClt.CurrentCluster().VnetConfigServiceClient() - return vnetConfigClt, nil + return clt, nil } diff --git a/lib/vnet/vnet_test.go b/lib/vnet/vnet_test.go index 3c10fedf4f2..a6c1a965548 100644 --- a/lib/vnet/vnet_test.go +++ b/lib/vnet/vnet_test.go @@ -291,9 +291,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf } if leafClusterName == "" { return &fakeClusterClient{ - clusterName: profileName, authClient: &fakeAuthClient{ - apps: rootCluster.apps, + clusterSpec: rootCluster, + clusterName: profileName, }, }, nil } @@ -302,9 +302,9 @@ func (p *echoAppProvider) GetCachedClient(ctx context.Context, profileName, leaf return nil, trace.NotFound("no cluster for %s.%s", profileName, leafClusterName) } return &fakeClusterClient{ - clusterName: leafClusterName, authClient: &fakeAuthClient{ - apps: leafCluster.apps, + clusterSpec: leafCluster, + clusterName: leafClusterName, }, }, nil } @@ -374,8 +374,7 @@ func (p *echoAppProvider) OnNewConnection(ctx context.Context, profileName, leaf } type fakeClusterClient struct { - authClient *fakeAuthClient - clusterName string + authClient *fakeAuthClient } func (c *fakeClusterClient) CurrentCluster() authclient.ClientI { @@ -383,19 +382,20 @@ func (c *fakeClusterClient) CurrentCluster() authclient.ClientI { } func (c *fakeClusterClient) ClusterName() string { - return c.clusterName + return c.authClient.clusterName } // fakeAuthClient is a fake auth client that answers GetResources requests with a static list of apps and // basic/faked predicate filtering. type fakeAuthClient struct { authclient.ClientI - apps []string + clusterSpec testClusterSpec + clusterName string } func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) { resp := &proto.ListResourcesResponse{} - for _, app := range c.apps { + for _, app := range c.clusterSpec.apps { // Poor-man's predicate expression filter. if !strings.Contains(req.PredicateExpression, app) { continue @@ -425,6 +425,39 @@ func (c *fakeAuthClient) GetResources(ctx context.Context, req *proto.ListResour return resp, nil } +func (c *fakeAuthClient) ListRemoteClusters(ctx context.Context, pageSize int, pageToken string) ([]types.RemoteCluster, string, error) { + remoteClusters := make([]types.RemoteCluster, 0, len(c.clusterSpec.leafClusters)) + for leafClusterName := range c.clusterSpec.leafClusters { + rc, err := types.NewRemoteCluster(leafClusterName) + if err != nil { + return nil, "", trace.Wrap(err) + } + remoteClusters = append(remoteClusters, rc) + } + return remoteClusters, "", nil +} + +func (c *fakeAuthClient) Ping(ctx context.Context) (proto.PingResponse, error) { + return proto.PingResponse{ + ClusterName: c.clusterName, + ProxyPublicAddr: c.clusterName, + }, nil +} + +func (c *fakeAuthClient) GetVnetConfig(ctx context.Context) (*vnet.VnetConfig, error) { + vnetConfig := &vnet.VnetConfig{ + Spec: &vnet.VnetConfigSpec{ + Ipv4CidrRange: c.clusterSpec.cidrRange, + }, + } + for _, zone := range c.clusterSpec.customDNSZones { + vnetConfig.Spec.CustomDnsZones = append(vnetConfig.Spec.CustomDnsZones, &vnet.CustomDNSZone{ + Suffix: zone, + }) + } + return vnetConfig, nil +} + func TestDialFakeApp(t *testing.T) { t.Parallel() ctx, cancel := context.WithCancel(context.Background()) @@ -460,13 +493,16 @@ func TestDialFakeApp(t *testing.T) { "leaf1.example.com": { apps: []string{"echo1.leaf1.example.com"}, }, + "leaf2.example.com": { + apps: []string{"echo1.leaf2.example.com"}, + }, }, }, "root2.example.com": { apps: []string{"echo1.root2.example.com", "echo2.root2.example.com"}, leafClusters: map[string]testClusterSpec{ - "leaf2.example.com": { - apps: []string{"echo1.leaf2.example.com"}, + "leaf3.example.com": { + apps: []string{"echo1.leaf3.example.com"}, }, }, }, @@ -500,6 +536,14 @@ func TestDialFakeApp(t *testing.T) { app: "echo.nested.myzone.example.com", expectCIDR: "192.168.2.0/24", }, + { + app: "echo1.leaf1.example.com", + expectCIDR: defaultIPv4CIDRRange, + }, + { + app: "echo1.leaf2.example.com", + expectCIDR: defaultIPv4CIDRRange, + }, { app: "echo1.root2.example.com", expectCIDR: defaultIPv4CIDRRange, @@ -508,6 +552,10 @@ func TestDialFakeApp(t *testing.T) { app: "echo2.root2.example.com", expectCIDR: defaultIPv4CIDRRange, }, + { + app: "echo1.leaf3.example.com", + expectCIDR: defaultIPv4CIDRRange, + }, } t.Run("valid", func(t *testing.T) { @@ -555,9 +603,6 @@ func TestDialFakeApp(t *testing.T) { "not.an.app.example.com.", "not.in.a.custom.zone", "in.an.invalid.zone", - // Leaf clusters not supported yet. - "echo1.leaf1.example.com.", - "echo2.leaf1.example.com.", } for _, fqdn := range invalidTestCases { t.Run(fqdn, func(t *testing.T) { diff --git a/tool/tsh/common/vnet_common.go b/tool/tsh/common/vnet_common.go index 027071b8109..6dae51fc692 100644 --- a/tool/tsh/common/vnet_common.go +++ b/tool/tsh/common/vnet_common.go @@ -27,7 +27,6 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport/api/client/proto" - vnetproto "github.com/gravitational/teleport/api/gen/proto/go/teleport/vnet/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/client/clientcache" @@ -116,16 +115,6 @@ func (p *vnetAppProvider) GetDialOptions(ctx context.Context, profileName string return dialOpts, nil } -func (p *vnetAppProvider) GetVnetConfig(ctx context.Context, profileName, leafClusterName string) (*vnetproto.VnetConfig, error) { - clusterClient, err := p.clientCache.Get(ctx, profileName, leafClusterName) - if err != nil { - return nil, trace.Wrap(err) - } - vnetConfigClient := clusterClient.AuthClient.VnetConfigServiceClient() - vnetConfig, err := vnetConfigClient.GetVnetConfig(ctx, &vnetproto.GetVnetConfigRequest{}) - return vnetConfig, trace.Wrap(err) -} - // OnNewConnection gets called before each VNet connection. It's a noop as tsh doesn't need to do // anything extra here. func (p *vnetAppProvider) OnNewConnection(ctx context.Context, profileName, leafClusterName string, app types.Application) error {