Skip to content
Open
69 changes: 69 additions & 0 deletions balancer/hostname/hostname.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

// Package hostname contains utilities for the endpoint hostname attribute
// (used for per-endpoint :authority / SNI override).
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
package hostname

import "google.golang.org/grpc/resolver"

type hostnameKey struct{}

// Set returns a copy of the given endpoint with the hostname attribute set.
// If hostname is empty the endpoint is returned unmodified.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func Set(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
if hostname == "" {
return endpoint
}
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKey{}, hostname)
return endpoint
}

// FromEndpoint returns the hostname attribute of endpoint. If this attribute is
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: wrap the comment in 80 columns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done

// not set, it returns the empty string.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func FromEndpoint(endpoint resolver.Endpoint) string {
h, _ := endpoint.Attributes.Value(hostnameKey{}).(string)
return h
}

// FromAddress returns the hostname attribute from a resolver.Address.
// It reads from the BalancerAttributes field of the address.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func FromAddress(addr resolver.Address) string {
h, _ := addr.BalancerAttributes.Value(hostnameKey{}).(string)
return h
}
81 changes: 81 additions & 0 deletions balancer/hostname/hostname_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
*
* Copyright 2026 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package hostname_test

import (
"testing"

"google.golang.org/grpc/attributes"
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/resolver"
)

type s struct {
grpctest.Tester
}

func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}

func (s) TestHostnameToAndFromEndpoint(t *testing.T) {
tests := []struct {
desc string
inputHostname string
inputAttributes *attributes.Attributes
wantHostname string
}{
{
desc: "empty_attributes",
inputHostname: "myservice.example.com",
inputAttributes: nil,
wantHostname: "myservice.example.com",
},
{
desc: "non-empty_attributes",
inputHostname: "myservice.example.com",
inputAttributes: attributes.New("foo", "bar"),
wantHostname: "myservice.example.com",
},
{
desc: "hostname_not_present_in_empty_attributes",
inputHostname: "",
inputAttributes: nil,
wantHostname: "",
},
{
desc: "hostname_not_present_in_non-empty_attributes",
inputHostname: "",
inputAttributes: attributes.New("foo", "bar"),
wantHostname: "",
},
}

for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
endpoint := resolver.Endpoint{Attributes: test.inputAttributes}
endpoint = hostname.Set(endpoint, test.inputHostname)
gotHostname := hostname.FromEndpoint(endpoint)
if gotHostname != test.wantHostname {
t.Errorf("gotHostname: %v, wantHostname: %v", gotHostname, test.wantHostname)
}
})
}
}
2 changes: 1 addition & 1 deletion balancer/pickfirst/pickfirst.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ import (

"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/pickfirst/internal"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
expstats "google.golang.org/grpc/experimental/stats"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
internalgrpclog "google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/pretty"
Expand Down
2 changes: 1 addition & 1 deletion balancer/pickfirst/pickfirst_ext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import (
"google.golang.org/grpc/balancer"
pfbalancer "google.golang.org/grpc/balancer/pickfirst"
pfinternal "google.golang.org/grpc/balancer/pickfirst/internal"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/balancer/stub"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/channelz"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
Expand Down
2 changes: 1 addition & 1 deletion balancer/ringhash/ring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"testing"

xxhash "github.com/cespare/xxhash/v2"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/resolver"
)

Expand Down
2 changes: 1 addition & 1 deletion balancer/ringhash/ringhash.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ import (
"google.golang.org/grpc/balancer/endpointsharding"
"google.golang.org/grpc/balancer/lazy"
"google.golang.org/grpc/balancer/pickfirst"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/pretty"
iringhash "google.golang.org/grpc/internal/ringhash"
Expand Down
2 changes: 1 addition & 1 deletion balancer/ringhash/ringhash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import (
"time"

"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/internal/grpctest"
iringhash "google.golang.org/grpc/internal/ringhash"
"google.golang.org/grpc/internal/testutils"
Expand Down
15 changes: 15 additions & 0 deletions internal/balancer/weight/weight.go → balancer/weight/weight.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
// Package weight contains utilities to manage endpoint weights. Weights are
// used by LB policies such as ringhash to distribute load across multiple
// endpoints.
//
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of putting a deprecated message and creating new files in balancer/weight/, use git move to move the files from internal/balancer/weight/weight.go to balancer/weight/weight.go.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

done - i also added the "Experimental" comment because this is a newly-public API. is that fine?

// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
package weight

import (
Expand All @@ -45,6 +50,11 @@ func (a EndpointInfo) Equal(o any) bool {

// Set returns a copy of endpoint in which the Attributes field is updated with
// EndpointInfo.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func Set(endpoint resolver.Endpoint, epInfo EndpointInfo) resolver.Endpoint {
endpoint.Attributes = endpoint.Attributes.WithValue(attributeKey{}, epInfo)
return endpoint
Expand All @@ -59,6 +69,11 @@ func (a EndpointInfo) String() string {

// FromEndpoint returns the EndpointInfo stored in the Attributes field of an
// endpoint. It returns an empty EndpointInfo if attribute is not found.
//
// # Experimental
//
// Notice: This API is EXPERIMENTAL and may be changed or removed in a
// later release.
func FromEndpoint(endpoint resolver.Endpoint) EndpointInfo {
v := endpoint.Attributes.Value(attributeKey{})
ei, _ := v.(EndpointInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (

"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/resolver"
)
Expand Down
5 changes: 3 additions & 2 deletions internal/xds/balancer/cdsbalancer/configbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
"maps"
"slices"

"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/hierarchy"
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
Expand Down Expand Up @@ -181,7 +182,7 @@ func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterC
// LB policies that rely on locality information (like weighted_target)
// continue to work.
localityStr := xdsinternal.LocalityString(clients.Locality{})
retEndpoint = xdsresource.SetHostname(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
// Set the locality weight to 1. This is required because the child policy
// like weighted_target which relies on locality weights to distribute
// traffic. These policies may drop traffic if the weight is 0.
Expand Down
2 changes: 1 addition & 1 deletion internal/xds/balancer/cdsbalancer/configbuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"google.golang.org/grpc/balancer/pickfirst"
"google.golang.org/grpc/balancer/ringhash"
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/internal/balancer/weight"
"google.golang.org/grpc/balancer/weight"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/hierarchy"
iringhash "google.golang.org/grpc/internal/ringhash"
Expand Down
11 changes: 6 additions & 5 deletions internal/xds/balancer/clusterimpl/clusterimpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"google.golang.org/grpc/internal/pretty"
xdsinternal "google.golang.org/grpc/internal/xds"

"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/internal/xds/balancer/clusterimpl/internal"
"google.golang.org/grpc/internal/xds/balancer/loadstore"
"google.golang.org/grpc/internal/xds/bootstrap"
Expand Down Expand Up @@ -575,23 +576,23 @@ func (b *clusterImplBalancer) NewSubConn(addrs []resolver.Address, opts balancer
newAddrs[i] = xdsinternal.SetXDSHandshakeClusterName(addr, clusterName)
newAddrs[i] = xds.SetHandshakeInfo(newAddrs[i], &b.xdsHIPtr)

hostname := xdsresource.Hostname(addr)
host := hostname.FromAddress(addr)
// If the hostname contains a port, strip it. Per [RFC 6066, Section
// 3](https://www.rfc-editor.org/rfc/rfc6066.html#section-3), the SNI
// may only contain a qualified DNS hostname, which excludes port
// numbers.
h, _, err := net.SplitHostPort(hostname)
h, _, err := net.SplitHostPort(host)
if err == nil {
hostname = h
host = h
}
// Store hostname in the address attributes, so that it can be used in
// the client handshake.
newAddrs[i] = xds.SetAddressHostname(newAddrs[i], hostname)
newAddrs[i] = xds.SetAddressHostname(newAddrs[i], host)
}
var sc balancer.SubConn
scw := &scWrapper{}
if len(addrs) > 0 {
scw.hostname = xdsresource.Hostname(addrs[0])
scw.hostname = hostname.FromAddress(addrs[0])
scw.localityID = xdsinternal.GetLocalityID(addrs[0])
}
oldListener := opts.StateListener
Expand Down
26 changes: 2 additions & 24 deletions internal/xds/xdsclient/xdsresource/unmarshal_eds.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/pretty"
xdsinternal "google.golang.org/grpc/internal/xds"
Expand All @@ -36,29 +37,6 @@ import (
"google.golang.org/protobuf/types/known/anypb"
)

// hostnameKeyType is the key to store the hostname attribute in
// a resolver.Endpoint.
type hostnameKeyType struct{}

// SetHostname returns a copy of the given endpoint with hostname added
// as an attribute.
func SetHostname(endpoint resolver.Endpoint, hostname string) resolver.Endpoint {
// Only set if non-empty; xds_cluster_impl uses this to trigger :authority
// rewriting.
if hostname == "" {
return endpoint
}
endpoint.Attributes = endpoint.Attributes.WithValue(hostnameKeyType{}, hostname)
return endpoint
}

// Hostname returns the hostname from the BalancerAttributes of the given
// Address. If this attribute is not set, it returns the empty string.
func Hostname(addr resolver.Address) string {
hostname, _ := addr.BalancerAttributes.Value(hostnameKeyType{}).(string)
return hostname
}

func unmarshalEndpointsResource(r *anypb.Any) (string, EndpointsUpdate, error) {
r, err := UnwrapResource(r)
if err != nil {
Expand Down Expand Up @@ -166,7 +144,7 @@ func parseEndpoints(lbEndpoints []*v3endpointpb.LbEndpoint, uniqueEndpointAddrs
}
}
endpoint := resolver.Endpoint{Addresses: address}
endpoint = SetHostname(endpoint, lbEndpoint.GetEndpoint().GetHostname())
endpoint = hostname.Set(endpoint, lbEndpoint.GetEndpoint().GetHostname())
endpoint = ringhash.SetHashKey(endpoint, hashKey)
endpoints = append(endpoints, Endpoint{
ResolverEndpoint: endpoint,
Expand Down
5 changes: 3 additions & 2 deletions internal/xds/xdsclient/xdsresource/unmarshal_eds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
v3typepb "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/balancer/hostname"
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/pretty"
"google.golang.org/grpc/internal/testutils"
Expand Down Expand Up @@ -62,13 +63,13 @@ func disableA86(t *testing.T) {
unregisterMetadataConverterForTesting(proxyAddressTypeURL)
}

func buildResolverEndpoint(addr []string, hostname string) resolver.Endpoint {
func buildResolverEndpoint(addr []string, host string) resolver.Endpoint {
address := []resolver.Address{}
for _, a := range addr {
address = append(address, resolver.Address{Addr: a})
}
resolverEndpoint := resolver.Endpoint{Addresses: address}
resolverEndpoint = SetHostname(resolverEndpoint, hostname)
resolverEndpoint = hostname.Set(resolverEndpoint, host)
return resolverEndpoint
}

Expand Down
Loading