Skip to content

Commit

Permalink
feat: Implement preserving header letter cases (envoyproxy#2506)
Browse files Browse the repository at this point in the history
Implement preserving header letter cases

Signed-off-by: Lior Okman <[email protected]>
Co-authored-by: zirain <[email protected]>
  • Loading branch information
liorokman and zirain authored Jan 27, 2024
1 parent 52c18f3 commit b33f09b
Show file tree
Hide file tree
Showing 13 changed files with 463 additions and 39 deletions.
3 changes: 2 additions & 1 deletion internal/gatewayapi/clienttrafficpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ func translateHTTP1Settings(http1Settings *egv1a1.HTTP1Settings, httpIR *ir.HTTP
return
}
httpIR.HTTP1 = &ir.HTTP1Settings{
EnableTrailers: ptr.Deref(http1Settings.EnableTrailers, false),
EnableTrailers: ptr.Deref(http1Settings.EnableTrailers, false),
PreserveHeaderCase: ptr.Deref(http1Settings.PreserveHeaderCase, false),
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
clientTrafficPolicies:
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
namespace: envoy-gateway
name: target-gateway-1
spec:
http1:
enableTrailers: true
preserveHeaderCase: true
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: gateway-1
namespace: envoy-gateway
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http-1
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
- name: http-2
protocol: HTTP
port: 8080
allowedRoutes:
namespaces:
from: Same
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
clientTrafficPolicies:
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
creationTimestamp: null
name: target-gateway-1
namespace: envoy-gateway
spec:
http1:
enableTrailers: true
preserveHeaderCase: true
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: gateway-1
namespace: envoy-gateway
status:
conditions:
- lastTransitionTime: null
message: ClientTrafficPolicy has been accepted.
reason: Accepted
status: "True"
type: Accepted
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
creationTimestamp: null
name: gateway-1
namespace: envoy-gateway
spec:
gatewayClassName: envoy-gateway-class
listeners:
- allowedRoutes:
namespaces:
from: Same
name: http-1
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
name: http-2
port: 8080
protocol: HTTP
status:
listeners:
- attachedRoutes: 0
conditions:
- lastTransitionTime: null
message: Sending translated listener configuration to the data plane
reason: Programmed
status: "True"
type: Programmed
- lastTransitionTime: null
message: Listener has been successfully translated
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: null
message: Listener references have been resolved
reason: ResolvedRefs
status: "True"
type: ResolvedRefs
name: http-1
supportedKinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
- group: gateway.networking.k8s.io
kind: GRPCRoute
- attachedRoutes: 0
conditions:
- lastTransitionTime: null
message: Sending translated listener configuration to the data plane
reason: Programmed
status: "True"
type: Programmed
- lastTransitionTime: null
message: Listener has been successfully translated
reason: Accepted
status: "True"
type: Accepted
- lastTransitionTime: null
message: Listener references have been resolved
reason: ResolvedRefs
status: "True"
type: ResolvedRefs
name: http-2
supportedKinds:
- group: gateway.networking.k8s.io
kind: HTTPRoute
- group: gateway.networking.k8s.io
kind: GRPCRoute
infraIR:
envoy-gateway/gateway-1:
proxy:
listeners:
- address: null
name: envoy-gateway/gateway-1/http-1
ports:
- containerPort: 10080
name: http-1
protocol: HTTP
servicePort: 80
- address: null
name: envoy-gateway/gateway-1/http-2
ports:
- containerPort: 8080
name: http-2
protocol: HTTP
servicePort: 8080
metadata:
labels:
gateway.envoyproxy.io/owning-gateway-name: gateway-1
gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
name: envoy-gateway/gateway-1
xdsIR:
envoy-gateway/gateway-1:
accessLog:
text:
- path: /dev/stdout
http:
- address: 0.0.0.0
hostnames:
- '*'
http1:
enableTrailers: true
preserveHeaderCase: true
isHTTP2: false
name: envoy-gateway/gateway-1/http-1
path:
escapedSlashesAction: UnescapeAndRedirect
mergeSlashes: true
port: 10080
- address: 0.0.0.0
hostnames:
- '*'
http1:
enableTrailers: true
preserveHeaderCase: true
isHTTP2: false
name: envoy-gateway/gateway-1/http-2
path:
escapedSlashesAction: UnescapeAndRedirect
mergeSlashes: true
port: 8080
3 changes: 2 additions & 1 deletion internal/ir/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ type HTTP3Settings struct {
// HTTP1Settings provides HTTP/1 configuration on the listener.
// +k8s:deepcopy-gen=true
type HTTP1Settings struct {
EnableTrailers bool `json:"enableTrailers,omitempty" yaml:"enableTrailers,omitempty"`
EnableTrailers bool `json:"enableTrailers,omitempty" yaml:"enableTrailers,omitempty"`
PreserveHeaderCase bool `json:"preserveHeaderCase,omitempty" yaml:"preserveHeaderCase,omitempty"`
}

// ListenerPort defines a network port of a listener.
Expand Down
66 changes: 38 additions & 28 deletions internal/xds/translator/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
proxyprotocolv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/proxy_protocol/v3"
rawbufferv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/raw_buffer/v3"
httpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
Expand All @@ -23,6 +24,7 @@ import (
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"k8s.io/utils/ptr"

"github.com/envoyproxy/gateway/internal/ir"
)
Expand All @@ -42,7 +44,7 @@ type xdsClusterArgs struct {
proxyProtocol *ir.ProxyProtocol
circuitBreaker *ir.CircuitBreaker
healthCheck *ir.HealthCheck
enableTrailers bool
http1Settings *ir.HTTP1Settings
}

type EndpointType int
Expand Down Expand Up @@ -96,7 +98,8 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
break
}
}
cluster.TypedExtensionProtocolOptions = buildTypedExtensionProtocolOptions(isHTTP2, args.enableTrailers)
cluster.TypedExtensionProtocolOptions = buildTypedExtensionProtocolOptions(isHTTP2,
ptr.Deref(args.http1Settings, ir.HTTP1Settings{}))

// Set Load Balancer policy
//nolint:gocritic
Expand Down Expand Up @@ -314,44 +317,51 @@ func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.Destin
return &endpointv3.ClusterLoadAssignment{ClusterName: clusterName, Endpoints: localities}
}

func buildTypedExtensionProtocolOptions(http2, http1Trailers bool) map[string]*anypb.Any {
func buildTypedExtensionProtocolOptions(http2 bool, http1Opts ir.HTTP1Settings) map[string]*anypb.Any {
if !http2 && !http1Opts.EnableTrailers && !http1Opts.PreserveHeaderCase {
return nil
}
var anyProtocolOptions *anypb.Any

protocolOptions := httpv3.HttpProtocolOptions{}
if http2 {
protocolOptions := httpv3.HttpProtocolOptions{
UpstreamProtocolOptions: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{},
},
protocolOptions.UpstreamProtocolOptions = &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_Http2ProtocolOptions{},
},
}

anyProtocolOptions, _ = anypb.New(&protocolOptions)
} else if http1Trailers {
// TODO: If the cluster is TLS enabled, use AutoHTTPConfig instead of ExplicitHttpConfig
// so that when ALPN is supported enabling trailers doesn't force HTTP/1.1
protocolOptions := httpv3.HttpProtocolOptions{
UpstreamProtocolOptions: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_HttpProtocolOptions{
HttpProtocolOptions: &corev3.Http1ProtocolOptions{
EnableTrailers: http1Trailers,
},
} else if http1Opts.EnableTrailers || http1Opts.PreserveHeaderCase {
opts := &corev3.Http1ProtocolOptions{
EnableTrailers: http1Opts.EnableTrailers,
}
if http1Opts.PreserveHeaderCase {
preservecaseAny, _ := anypb.New(&preservecasev3.PreserveCaseFormatterConfig{})
opts.HeaderKeyFormat = &corev3.Http1ProtocolOptions_HeaderKeyFormat{
HeaderFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter{
StatefulFormatter: &corev3.TypedExtensionConfig{
Name: "preserve_case",
TypedConfig: preservecaseAny,
},
},
}
}
// TODO: If the cluster is TLS enabled, use AutoHTTPConfig instead of ExplicitHttpConfig
// so that when ALPN is supported setting HTTP/1.1 options doesn't force HTTP/1.1
protocolOptions.UpstreamProtocolOptions = &httpv3.HttpProtocolOptions_ExplicitHttpConfig_{
ExplicitHttpConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig{
ProtocolConfig: &httpv3.HttpProtocolOptions_ExplicitHttpConfig_HttpProtocolOptions{
HttpProtocolOptions: opts,
},
},
}
anyProtocolOptions, _ = anypb.New(&protocolOptions)
}
anyProtocolOptions, _ = anypb.New(&protocolOptions)

if anyProtocolOptions != nil {
extensionOptions := map[string]*anypb.Any{
extensionOptionsKey: anyProtocolOptions,
}

return extensionOptions
extensionOptions := map[string]*anypb.Any{
extensionOptionsKey: anyProtocolOptions,
}
return nil

return extensionOptions
}

// buildClusterName returns a cluster name for the given `host` and `port`.
Expand Down
21 changes: 17 additions & 4 deletions internal/xds/translator/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
tcpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/tcp_proxy/v3"
udpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/udp/udp_proxy/v3"
preservecasev3 "github.com/envoyproxy/go-control-plane/envoy/extensions/http/header_formatters/preserve_case/v3"
quicv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/quic/v3"
tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
Expand Down Expand Up @@ -44,12 +45,24 @@ func http1ProtocolOptions(opts *ir.HTTP1Settings) *corev3.Http1ProtocolOptions {
if opts == nil {
return nil
}
if opts.EnableTrailers {
return &corev3.Http1ProtocolOptions{
EnableTrailers: opts.EnableTrailers,
if !opts.EnableTrailers && !opts.PreserveHeaderCase {
return nil
}
r := &corev3.Http1ProtocolOptions{
EnableTrailers: opts.EnableTrailers,
}
if opts.PreserveHeaderCase {
preservecaseAny, _ := anypb.New(&preservecasev3.PreserveCaseFormatterConfig{})
r.HeaderKeyFormat = &corev3.Http1ProtocolOptions_HeaderKeyFormat{
HeaderFormat: &corev3.Http1ProtocolOptions_HeaderKeyFormat_StatefulFormatter{
StatefulFormatter: &corev3.TypedExtensionConfig{
Name: "preserve_case",
TypedConfig: preservecaseAny,
},
},
}
}
return nil
return r
}

func http2ProtocolOptions() *corev3.Http2ProtocolOptions {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
http:
- name: "first-listener"
address: "0.0.0.0"
port: 10080
hostnames:
- "*"
http1:
preserveHeaderCase: true
path:
mergeSlashes: true
escapedSlashesAction: UnescapeAndRedirect
routes:
- name: "first-route"
hostname: "*"
destination:
name: "first-route-dest"
settings:
- endpoints:
- host: "1.2.3.4"
port: 50000
- name: "second-listener"
address: "0.0.0.0"
port: 10081
hostnames:
- "*"
http1:
preserveHeaderCase: true
enableTrailers: true
path:
mergeSlashes: true
escapedSlashesAction: UnescapeAndRedirect
routes:
- name: "second-route"
hostname: "*"
destination:
name: "second-route-dest"
settings:
- endpoints:
- host: "1.2.3.5"
port: 50000
Loading

0 comments on commit b33f09b

Please sign in to comment.