blob: 077c32bb46ffff5325809bcc667dc6a62b2f2ab2 [file] [log] [blame]
// Copyright Istio 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 util
import (
"fmt"
"net"
"sort"
"strconv"
"strings"
)
import (
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
http_conn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
matcher "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
meshconfig "istio.io/api/mesh/v1alpha1"
networking "istio.io/api/networking/v1alpha3"
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/features"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
istionetworking "github.com/apache/dubbo-go-pixiu/pilot/pkg/networking"
"github.com/apache/dubbo-go-pixiu/pkg/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/config"
"github.com/apache/dubbo-go-pixiu/pkg/config/labels"
"github.com/apache/dubbo-go-pixiu/pkg/network"
"github.com/apache/dubbo-go-pixiu/pkg/proto/merge"
"github.com/apache/dubbo-go-pixiu/pkg/util/strcase"
)
const (
// BlackHoleCluster to catch traffic from routes with unresolved clusters. Traffic arriving here goes nowhere.
BlackHoleCluster = "BlackHoleCluster"
// BlackHole is the name of the virtual host and route name used to block all traffic
BlackHole = "block_all"
// PassthroughCluster to forward traffic to the original destination requested. This cluster is used when
// traffic does not match any listener in envoy.
PassthroughCluster = "PassthroughCluster"
// Passthrough is the name of the virtual host used to forward traffic to the
// PassthroughCluster
Passthrough = "allow_any"
// PassthroughFilterChain to catch traffic that doesn't match other filter chains.
PassthroughFilterChain = "PassthroughFilterChain"
// Inbound pass through cluster need to the bind the loopback ip address for the security and loop avoidance.
InboundPassthroughClusterIpv4 = "InboundPassthroughClusterIpv4"
InboundPassthroughClusterIpv6 = "InboundPassthroughClusterIpv6"
// SniClusterFilter is the name of the sni_cluster envoy filter
SniClusterFilter = "envoy.filters.network.sni_cluster"
// IstioMetadataKey is the key under which metadata is added to a route or cluster
// regarding the virtual service or destination rule used for each
IstioMetadataKey = "istio"
// EnvoyTransportSocketMetadataKey is the key under which metadata is added to an endpoint
// which determines the endpoint level transport socket configuration.
EnvoyTransportSocketMetadataKey = "envoy.transport_socket_match"
// EnvoyRawBufferSocketName matched with hardcoded built-in Envoy transport name which determines
// endpoint level plantext transport socket configuration
EnvoyRawBufferSocketName = wellknown.TransportSocketRawBuffer
// EnvoyTLSSocketName matched with hardcoded built-in Envoy transport name which determines endpoint
// level tls transport socket configuration
EnvoyTLSSocketName = wellknown.TransportSocketTls
// EnvoyQUICSocketName matched with hardcoded built-in Envoy transport name which determines endpoint
// level QUIC transport socket configuration
EnvoyQUICSocketName = wellknown.TransportSocketQuic
// Well-known header names
AltSvcHeader = "alt-svc"
)
// ALPNH2Only advertises that Proxy is going to use HTTP/2 when talking to the cluster.
var ALPNH2Only = []string{"h2"}
// ALPNInMeshH2 advertises that Proxy is going to use HTTP/2 when talking to the in-mesh cluster.
// The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions.
// Once Envoy supports client-side ALPN negotiation, this should be {"istio", "h2", "http/1.1"}.
var ALPNInMeshH2 = []string{"istio", "h2"}
// ALPNInMeshH2WithMxc advertises that Proxy is going to use HTTP/2 when talking to the in-mesh cluster.
// The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions.
// The custom "istio-peer-exchange" value indicates, metadata exchange is enabled for TCP.
var ALPNInMeshH2WithMxc = []string{"istio-peer-exchange", "istio", "h2"}
// ALPNInMesh advertises that Proxy is going to talk to the in-mesh cluster.
// The custom "istio" value indicates in-mesh traffic and it's going to be used for routing decisions.
var ALPNInMesh = []string{"istio"}
// ALPNInMeshWithMxc advertises that Proxy is going to talk to the in-mesh cluster and has metadata exchange enabled for
// TCP. The custom "istio-peer-exchange" value indicates, metadata exchange is enabled for TCP. The custom "istio" value
// indicates in-mesh traffic and it's going to be used for routing decisions.
var ALPNInMeshWithMxc = []string{"istio-peer-exchange", "istio"}
// ALPNHttp advertises that Proxy is going to talking either http2 or http 1.1.
var ALPNHttp = []string{"h2", "http/1.1"}
// ALPNHttp3OverQUIC advertises that Proxy is going to talk HTTP/3 over QUIC
var ALPNHttp3OverQUIC = []string{"h3"}
// ALPNDownstreamWithMxc advertises that Proxy is going to talk either tcp(for metadata exchange), http2 or http 1.1.
var ALPNDownstreamWithMxc = []string{"istio-peer-exchange", "h2", "http/1.1"}
// ALPNDownstream advertises that Proxy is going to talk http2 or http 1.1.
var ALPNDownstream = []string{"h2", "http/1.1"}
// RegexEngine is the default google RE2 regex engine.
var RegexEngine = &matcher.RegexMatcher_GoogleRe2{GoogleRe2: &matcher.RegexMatcher_GoogleRE2{}}
func getMaxCidrPrefix(addr string) uint32 {
ip := net.ParseIP(addr)
if ip.To4() == nil {
// ipv6 address
return 128
}
// ipv4 address
return 32
}
func ListContains(haystack []string, needle string) bool {
for _, n := range haystack {
if needle == n {
return true
}
}
return false
}
// ConvertAddressToCidr converts from string to CIDR proto
func ConvertAddressToCidr(addr string) *core.CidrRange {
if len(addr) == 0 {
return nil
}
cidr := &core.CidrRange{
AddressPrefix: addr,
PrefixLen: &wrapperspb.UInt32Value{
Value: getMaxCidrPrefix(addr),
},
}
if strings.Contains(addr, "/") {
parts := strings.Split(addr, "/")
cidr.AddressPrefix = parts[0]
prefix, _ := strconv.Atoi(parts[1])
cidr.PrefixLen.Value = uint32(prefix)
}
return cidr
}
// BuildAddress returns a SocketAddress with the given ip and port or uds.
func BuildAddress(bind string, port uint32) *core.Address {
address := BuildNetworkAddress(bind, port, istionetworking.TransportProtocolTCP)
if address != nil {
return address
}
return &core.Address{
Address: &core.Address_Pipe{
Pipe: &core.Pipe{
Path: strings.TrimPrefix(bind, model.UnixAddressPrefix),
},
},
}
}
func BuildNetworkAddress(bind string, port uint32, transport istionetworking.TransportProtocol) *core.Address {
if port == 0 {
return nil
}
return &core.Address{
Address: &core.Address_SocketAddress{
SocketAddress: &core.SocketAddress{
Address: bind,
Protocol: transport.ToEnvoySocketProtocol(),
PortSpecifier: &core.SocketAddress_PortValue{
PortValue: port,
},
},
},
}
}
// MessageToAnyWithError converts from proto message to proto Any
func MessageToAnyWithError(msg proto.Message) (*anypb.Any, error) {
b, err := proto.MarshalOptions{Deterministic: true}.Marshal(msg)
if err != nil {
return nil, err
}
return &anypb.Any{
// nolint: staticcheck
TypeUrl: "type.googleapis.com/" + string(msg.ProtoReflect().Descriptor().FullName()),
Value: b,
}, nil
}
// MessageToAny converts from proto message to proto Any
func MessageToAny(msg proto.Message) *anypb.Any {
out, err := MessageToAnyWithError(msg)
if err != nil {
log.Error(fmt.Sprintf("error marshaling Any %s: %v", prototext.Format(msg), err))
return nil
}
return out
}
// SortVirtualHosts sorts a slice of virtual hosts by name.
//
// Envoy computes a hash of RDS to see if things have changed - hash is affected by order of elements in the filter. Therefore
// we sort virtual hosts by name before handing them back so the ordering is stable across HTTP Route Configs.
func SortVirtualHosts(hosts []*route.VirtualHost) {
if len(hosts) < 2 {
return
}
sort.SliceStable(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
}
// IsIstioVersionGE114 checks whether the given Istio version is greater than or equals 1.14.
func IsIstioVersionGE114(version *model.IstioVersion) bool {
return version == nil ||
version.Compare(&model.IstioVersion{Major: 1, Minor: 14, Patch: -1}) >= 0
}
func IsProtocolSniffingEnabledForPort(port *model.Port) bool {
return features.EnableProtocolSniffingForOutbound && port.Protocol.IsUnsupported()
}
func IsProtocolSniffingEnabledForInboundPort(port *model.Port) bool {
return features.EnableProtocolSniffingForInbound && port.Protocol.IsUnsupported()
}
func IsProtocolSniffingEnabledForOutboundPort(port *model.Port) bool {
return features.EnableProtocolSniffingForOutbound && port.Protocol.IsUnsupported()
}
// ConvertLocality converts '/' separated locality string to Locality struct.
func ConvertLocality(locality string) *core.Locality {
if locality == "" {
return &core.Locality{}
}
region, zone, subzone := model.SplitLocalityLabel(locality)
return &core.Locality{
Region: region,
Zone: zone,
SubZone: subzone,
}
}
// LocalityToString converts Locality struct to '/' separated locality string.
func LocalityToString(l *core.Locality) string {
if l == nil {
return ""
}
resp := l.Region
if l.Zone == "" {
return resp
}
resp += "/" + l.Zone
if l.SubZone == "" {
return resp
}
resp += "/" + l.SubZone
return resp
}
// IsLocalityEmpty checks if a locality is empty (checking region is good enough, based on how its initialized)
func IsLocalityEmpty(locality *core.Locality) bool {
if locality == nil || (len(locality.GetRegion()) == 0) {
return true
}
return false
}
func LocalityMatch(proxyLocality *core.Locality, ruleLocality string) bool {
ruleRegion, ruleZone, ruleSubzone := model.SplitLocalityLabel(ruleLocality)
regionMatch := ruleRegion == "*" || proxyLocality.GetRegion() == ruleRegion
zoneMatch := ruleZone == "*" || ruleZone == "" || proxyLocality.GetZone() == ruleZone
subzoneMatch := ruleSubzone == "*" || ruleSubzone == "" || proxyLocality.GetSubZone() == ruleSubzone
if regionMatch && zoneMatch && subzoneMatch {
return true
}
return false
}
func LbPriority(proxyLocality, endpointsLocality *core.Locality) int {
if proxyLocality.GetRegion() == endpointsLocality.GetRegion() {
if proxyLocality.GetZone() == endpointsLocality.GetZone() {
if proxyLocality.GetSubZone() == endpointsLocality.GetSubZone() {
return 0
}
return 1
}
return 2
}
return 3
}
// return a shallow copy ClusterLoadAssignment
func CloneClusterLoadAssignment(original *endpoint.ClusterLoadAssignment) *endpoint.ClusterLoadAssignment {
if original == nil {
return nil
}
out := &endpoint.ClusterLoadAssignment{}
out.ClusterName = original.ClusterName
out.Endpoints = cloneLocalityLbEndpoints(original.Endpoints)
out.Policy = original.Policy
return out
}
// return a shallow copy LocalityLbEndpoints
func cloneLocalityLbEndpoints(endpoints []*endpoint.LocalityLbEndpoints) []*endpoint.LocalityLbEndpoints {
out := make([]*endpoint.LocalityLbEndpoints, 0, len(endpoints))
for _, ep := range endpoints {
clone := CloneLocalityLbEndpoint(ep)
out = append(out, clone)
}
return out
}
// return a shallow copy of LocalityLbEndpoints
func CloneLocalityLbEndpoint(ep *endpoint.LocalityLbEndpoints) *endpoint.LocalityLbEndpoints {
clone := &endpoint.LocalityLbEndpoints{}
clone.Locality = ep.Locality
clone.LbEndpoints = ep.LbEndpoints
clone.Proximity = ep.Proximity
clone.Priority = ep.Priority
if ep.LoadBalancingWeight != nil {
clone.LoadBalancingWeight = &wrapperspb.UInt32Value{
Value: ep.GetLoadBalancingWeight().GetValue(),
}
}
return clone
}
// BuildConfigInfoMetadata builds core.Metadata struct containing the
// name.namespace of the config, the type, etc.
func BuildConfigInfoMetadata(config config.Meta) *core.Metadata {
return AddConfigInfoMetadata(nil, config)
}
// AddConfigInfoMetadata adds name.namespace of the config, the type, etc
// to the given core.Metadata struct, if metadata is not initialized, build a new metadata.
func AddConfigInfoMetadata(metadata *core.Metadata, config config.Meta) *core.Metadata {
if metadata == nil {
metadata = &core.Metadata{
FilterMetadata: map[string]*structpb.Struct{},
}
}
s := "/apis/" + config.GroupVersionKind.Group + "/" + config.GroupVersionKind.Version + "/namespaces/" + config.Namespace + "/" +
strcase.CamelCaseToKebabCase(config.GroupVersionKind.Kind) + "/" + config.Name
if _, ok := metadata.FilterMetadata[IstioMetadataKey]; !ok {
metadata.FilterMetadata[IstioMetadataKey] = &structpb.Struct{
Fields: map[string]*structpb.Value{},
}
}
metadata.FilterMetadata[IstioMetadataKey].Fields["config"] = &structpb.Value{
Kind: &structpb.Value_StringValue{
StringValue: s,
},
}
return metadata
}
// AddSubsetToMetadata will insert the subset name supplied. This should be called after the initial
// "istio" metadata has been created for the cluster. If the "istio" metadata field is not already
// defined, the subset information will not be added (to prevent adding this information where not
// needed). This is used for telemetry reporting.
func AddSubsetToMetadata(md *core.Metadata, subset string) {
if istioMeta, ok := md.FilterMetadata[IstioMetadataKey]; ok {
istioMeta.Fields["subset"] = &structpb.Value{
Kind: &structpb.Value_StringValue{
StringValue: subset,
},
}
}
}
// IsHTTPFilterChain returns true if the filter chain contains a HTTP connection manager filter
func IsHTTPFilterChain(filterChain *listener.FilterChain) bool {
for _, f := range filterChain.Filters {
if f.Name == wellknown.HTTPConnectionManager {
return true
}
}
return false
}
// MergeAnyWithAny merges a given any typed message into the given Any typed message by dynamically inferring the
// type of Any
func MergeAnyWithAny(dst *anypb.Any, src *anypb.Any) (*anypb.Any, error) {
// Assuming that Pilot is compiled with this type [which should always be the case]
var err error
// get an object of type used by this message
dstX, err := dst.UnmarshalNew()
if err != nil {
return nil, err
}
// get an object of type used by this message
srcX, err := src.UnmarshalNew()
if err != nil {
return nil, err
}
// Merge the two typed protos
merge.Merge(dstX, srcX)
var retVal *anypb.Any
// Convert the merged proto back to dst
if retVal, err = anypb.New(dstX); err != nil {
return nil, err
}
return retVal, nil
}
// BuildLbEndpointMetadata adds metadata values to a lb endpoint
func BuildLbEndpointMetadata(networkID network.ID, tlsMode, workloadname, namespace string,
clusterID cluster.ID, labels labels.Instance) *core.Metadata {
if networkID == "" && (tlsMode == "" || tlsMode == model.DisabledTLSModeLabel) &&
(!features.EndpointTelemetryLabel || !features.EnableTelemetryLabel) {
return nil
}
metadata := &core.Metadata{
FilterMetadata: map[string]*structpb.Struct{},
}
if tlsMode != "" && tlsMode != model.DisabledTLSModeLabel {
metadata.FilterMetadata[EnvoyTransportSocketMetadataKey] = &structpb.Struct{
Fields: map[string]*structpb.Value{
model.TLSModeLabelShortname: {Kind: &structpb.Value_StringValue{StringValue: tlsMode}},
},
}
}
// Add compressed telemetry metadata. Note this is a short term solution to make server workload metadata
// available at client sidecar, so that telemetry filter could use for metric labels. This is useful for two cases:
// server does not have sidecar injected, and request fails to reach server and thus metadata exchange does not happen.
// Due to performance concern, telemetry metadata is compressed into a semicolon separted string:
// workload-name;namespace;canonical-service-name;canonical-service-revision;cluster-id.
if features.EndpointTelemetryLabel {
var sb strings.Builder
sb.WriteString(workloadname)
sb.WriteString(";")
sb.WriteString(namespace)
sb.WriteString(";")
if csn, ok := labels[model.IstioCanonicalServiceLabelName]; ok {
sb.WriteString(csn)
}
sb.WriteString(";")
if csr, ok := labels[model.IstioCanonicalServiceRevisionLabelName]; ok {
sb.WriteString(csr)
}
sb.WriteString(";")
sb.WriteString(clusterID.String())
addIstioEndpointLabel(metadata, "workload", &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: sb.String()}})
}
return metadata
}
// MaybeApplyTLSModeLabel may or may not update the metadata for the Envoy transport socket matches for auto mTLS.
func MaybeApplyTLSModeLabel(ep *endpoint.LbEndpoint, tlsMode string) (*endpoint.LbEndpoint, bool) {
if ep == nil || ep.Metadata == nil {
return nil, false
}
epTLSMode := ""
if ep.Metadata.FilterMetadata != nil {
if v, ok := ep.Metadata.FilterMetadata[EnvoyTransportSocketMetadataKey]; ok {
epTLSMode = v.Fields[model.TLSModeLabelShortname].GetStringValue()
}
}
// Normalize the tls label name before comparison. This ensure we won't falsely cloning
// the endpoint when they are "" and model.DisabledTLSModeLabel.
if epTLSMode == model.DisabledTLSModeLabel {
epTLSMode = ""
}
if tlsMode == model.DisabledTLSModeLabel {
tlsMode = ""
}
if epTLSMode == tlsMode {
return nil, false
}
// We make a copy instead of modifying on existing endpoint pointer directly to avoid data race.
// See https://github.com/istio/istio/issues/34227 for details.
newEndpoint := proto.Clone(ep).(*endpoint.LbEndpoint)
if tlsMode != "" && tlsMode != model.DisabledTLSModeLabel {
newEndpoint.Metadata.FilterMetadata[EnvoyTransportSocketMetadataKey] = &structpb.Struct{
Fields: map[string]*structpb.Value{
model.TLSModeLabelShortname: {Kind: &structpb.Value_StringValue{StringValue: tlsMode}},
},
}
} else {
delete(newEndpoint.Metadata.FilterMetadata, EnvoyTransportSocketMetadataKey)
}
return newEndpoint, true
}
func addIstioEndpointLabel(metadata *core.Metadata, key string, val *structpb.Value) {
if _, ok := metadata.FilterMetadata[IstioMetadataKey]; !ok {
metadata.FilterMetadata[IstioMetadataKey] = &structpb.Struct{
Fields: map[string]*structpb.Value{},
}
}
metadata.FilterMetadata[IstioMetadataKey].Fields[key] = val
}
// IsAllowAnyOutbound checks if allow_any is enabled for outbound traffic
func IsAllowAnyOutbound(node *model.Proxy) bool {
return node.SidecarScope != nil &&
node.SidecarScope.OutboundTrafficPolicy != nil &&
node.SidecarScope.OutboundTrafficPolicy.Mode == networking.OutboundTrafficPolicy_ALLOW_ANY
}
func StringToExactMatch(in []string) []*matcher.StringMatcher {
if len(in) == 0 {
return nil
}
res := make([]*matcher.StringMatcher, 0, len(in))
for _, s := range in {
res = append(res, &matcher.StringMatcher{
MatchPattern: &matcher.StringMatcher_Exact{Exact: s},
})
}
return res
}
func StringToPrefixMatch(in []string) []*matcher.StringMatcher {
if len(in) == 0 {
return nil
}
res := make([]*matcher.StringMatcher, 0, len(in))
for _, s := range in {
res = append(res, &matcher.StringMatcher{
MatchPattern: &matcher.StringMatcher_Prefix{Prefix: s},
})
}
return res
}
func ConvertToEnvoyMatches(in []*networking.StringMatch) []*matcher.StringMatcher {
res := make([]*matcher.StringMatcher, 0, len(in))
for _, im := range in {
if em := ConvertToEnvoyMatch(im); em != nil {
res = append(res, em)
}
}
return res
}
func ConvertToEnvoyMatch(in *networking.StringMatch) *matcher.StringMatcher {
switch m := in.MatchType.(type) {
case *networking.StringMatch_Exact:
return &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Exact{Exact: m.Exact}}
case *networking.StringMatch_Prefix:
return &matcher.StringMatcher{MatchPattern: &matcher.StringMatcher_Prefix{Prefix: m.Prefix}}
case *networking.StringMatch_Regex:
return &matcher.StringMatcher{
MatchPattern: &matcher.StringMatcher_SafeRegex{
SafeRegex: &matcher.RegexMatcher{
EngineType: RegexEngine,
Regex: m.Regex,
},
},
}
}
return nil
}
func StringSliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func UInt32SliceEqual(a, b []uint32) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func CidrRangeSliceEqual(a, b []*core.CidrRange) bool {
if len(a) != len(b) {
return false
}
for i := range a {
netA, err := toIPNet(a[i])
if err != nil {
return false
}
netB, err := toIPNet(b[i])
if err != nil {
return false
}
if netA.IP.String() != netB.IP.String() {
return false
}
}
return true
}
func toIPNet(c *core.CidrRange) (*net.IPNet, error) {
_, cA, err := net.ParseCIDR(c.AddressPrefix + "/" + strconv.Itoa(int(c.PrefixLen.GetValue())))
if err != nil {
log.Errorf("failed to parse CidrRange %v as IPNet: %v", c, err)
}
return cA, err
}
// meshconfig ForwardClientCertDetails and the Envoy config enum are off by 1
// due to the UNDEFINED in the meshconfig ForwardClientCertDetails
func MeshConfigToEnvoyForwardClientCertDetails(c meshconfig.Topology_ForwardClientCertDetails) http_conn.HttpConnectionManager_ForwardClientCertDetails {
return http_conn.HttpConnectionManager_ForwardClientCertDetails(c - 1)
}
// ByteCount returns a human readable byte format
// Inspired by https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
func ByteCount(b int) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%dB", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f%cB",
float64(b)/float64(div), "kMGTPE"[exp])
}
// IPv6Compliant encloses ipv6 addresses in square brackets followed by port number in Host header/URIs
func IPv6Compliant(host string) string {
if strings.Contains(host, ":") {
return "[" + host + "]"
}
return host
}
// DomainName builds the domain name for a given host and port
func DomainName(host string, port int) string {
return net.JoinHostPort(host, strconv.Itoa(port))
}