Envoy的ext_proc
本篇文章中,我们学习Envoy中的ext_proc功能。
基本功能
首先我们搭建一个最简单的ext_proc,并观察他的基本使用方法。
首先我们搭建一个简单的外部后端。该后端只处理response header,他在原有header的基础上加一个x-extproc-helloheader。
package main
import (
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
)
type extProcServer struct {
extProcPb.UnimplementedExternalProcessorServer
}
// Process handles external processing requests from Envoy.
// It listens for incoming requests, modifies response headers,
// and sends the updated response back to Envoy.
//
// When a request with response headers is received, it adds a custom header
// "x-extproc-hello" with the value "Hello from ext_proc" and returns the modified headers.
//
// Note: The `RawValue` field is used instead of `Value` because it supports
// setting the header value as a byte slice, allowing precise handling of binary data.
//
// This function is called once per HTTP request to process gRPC messages from Envoy.
// It exits when an error occurs while receiving or sending messages.
func (s *extProcServer) Process(
srv extProcPb.ExternalProcessor_ProcessServer,
) error {
for {
req, err := srv.Recv()
if err != nil {
return status.Errorf(codes.Unknown, "error receiving request: %v", err)
}
log.Printf("Received request: %+v\n", req)
// Prepare the response to be returned to Envoy.
resp := &extProcPb.ProcessingResponse{}
// Only process response headers, not request headers.
if respHeaders := req.GetResponseHeaders(); respHeaders != nil {
log.Println("Processing Response Headers...")
resp = &extProcPb.ProcessingResponse{
Response: &extProcPb.ProcessingResponse_ResponseHeaders{
ResponseHeaders: &extProcPb.HeadersResponse{
Response: &extProcPb.CommonResponse{
HeaderMutation: &extProcPb.HeaderMutation{
SetHeaders: []*configPb.HeaderValueOption{
{
Header: &configPb.HeaderValue{
Key: "x-extproc-hello",
RawValue: []byte("Hello from ext_proc"),
},
},
},
},
},
},
},
}
log.Printf("Sending response: %+v\n", resp)
// Send the response back to Envoy.
if err := srv.Send(resp); err != nil {
return status.Errorf(codes.Unknown, "error sending response: %v", err)
}
} else {
// If it is not a callback in the response header stage, do not make any modifications and continue processing the next event.
// For request_headers or other events, do not modify & ensure that Envoy will not be stuck.
// An empty processing can be returned for request_headers, or it can be skipped in envoy.yaml.
// Here, simply continue to wait for the next event.
log.Printf("Not sending response %+v\n", resp)
continue
}
}
}
func main() {
lis, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
// Register the ExternalProcessorServer with the gRPC server.
extProcPb.RegisterExternalProcessorServer(grpcServer, &extProcServer{})
log.Println("Starting gRPC server on :9000...")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}然后我们配置一个本地的Envoy实例。该实例会call上面定义的外部后端进行处理。
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 8082
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%RESP(X-EXTPROC-HELLO)%\" \"%RESP(CONTENT-TYPE)%\" \"%RESP(CONTENT-LENGTH)%\" %DURATION% ms\n"
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: [ "*" ]
routes:
- match:
prefix: "/"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io
http_filters:
- name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
grpc_service:
envoy_grpc:
cluster_name: ext_proc_cluster
failure_mode_allow: true
processing_mode:
request_header_mode: SKIP
response_header_mode: SEND
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: ext_proc_cluster
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: ext_proc_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9000
- name: service_envoyproxy_io
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: service_envoyproxy_io
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: www.envoyproxy.io
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: www.envoyproxy.io分别使用envoy -c envoy.yaml和go run main.go启动Envoy和后端。当我们使用curl -I http://localhost:8082call Envoy的时候我们会看到如下header。
zheyu@ZhedeAir envoyext_proc % curl -I http://localhost:8082
HTTP/1.1 200 OK
accept-ranges: bytes
age: 8154
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; hit
content-length: 15991
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Wed, 29 Apr 2026 02:48:51 GMT
etag: "7ffdbb8473232c16f9299e0ceaf9b2cf-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01KQBJ79M7HD2HD1KCP4VY3SFN
x-envoy-upstream-service-time: 103
x-extproc-hello: Hello from ext_proc我们观察到新的x-extproc-helloheader已经被加了上去。
运行时跳过/运行ext_proc
这个部分里我们研究如何在运行时动态的跳过或者运行运行ext_proc。
基于route
我们将上面的Envoy配置修改成如下方式。
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 8082
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%RESP(X-EXTPROC-HELLO)%\" \"%RESP(CONTENT-TYPE)%\" \"%RESP(CONTENT-LENGTH)%\" %DURATION% ms\n"
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: [ "*" ]
routes:
# 重点 1:先匹配带有特定 Header 的请求
- match:
prefix: "/"
headers:
- name: "x-run-ext"
exact_match: "false"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io
# 重点 2:针对这条路由,禁用 ext_proc 过滤器
typed_per_filter_config:
envoy.filters.http.ext_proc:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExtProcPerRoute
disabled: true
# 重点 3:默认路由,不带 header 或值不为 true 的请求会走这里,正常触发 ext_proc
- match:
prefix: "/"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io
http_filters:
- name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
grpc_service:
envoy_grpc:
cluster_name: ext_proc_cluster
failure_mode_allow: true
processing_mode:
request_header_mode: SKIP
response_header_mode: SEND
- name: envoy.filters.http.ext_proc_required
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
grpc_service:
envoy_grpc:
cluster_name: ext_proc_cluster2
failure_mode_allow: true
processing_mode:
request_header_mode: SKIP
response_header_mode: SEND
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: ext_proc_cluster
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: ext_proc_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9000
- name: ext_proc_cluster2
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: { }
load_assignment:
cluster_name: ext_proc_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9002
- name: service_envoyproxy_io
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: service_envoyproxy_io
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: www.envoyproxy.io
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: www.envoyproxy.io注意到上面的Envoy:
- 配置了两个ext_proc filter。一个是
envoy.filters.http.ext_proc另一个是envoy.filters.http.ext_proc_required。对第一个filter,我们加了一个per route 配置,如果发现x-run-ext为false,则不run这个filter。 - 我们启动两个外部后端实例,分别对应上面两个filter。
接下来,我们修改外部后端代码,使其可以从外部接受端口,并且可以动态的决定要加什么header。
package main
import (
"flag"
"fmt"
"log"
"net"
configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type extProcServer struct {
extProcPb.UnimplementedExternalProcessorServer
// 新增字段,用于存储逻辑后缀
headerSuffix string
}
func (s *extProcServer) Process(
srv extProcPb.ExternalProcessor_ProcessServer,
) error {
for {
req, err := srv.Recv()
if err != nil {
return status.Errorf(codes.Unknown, "error receiving request: %v", err)
}
log.Printf("Received request: %+v\n", req)
resp := &extProcPb.ProcessingResponse{}
if respHeaders := req.GetResponseHeaders(); respHeaders != nil {
log.Println("Processing Response Headers...")
// 根据传入的参数构造 Header 内容
headerValue := "Hello from ext_proc"
header := fmt.Sprintf("x-extproc-hello-%s", s.headerSuffix)
resp = &extProcPb.ProcessingResponse{
Response: &extProcPb.ProcessingResponse_ResponseHeaders{
ResponseHeaders: &extProcPb.HeadersResponse{
Response: &extProcPb.CommonResponse{
HeaderMutation: &extProcPb.HeaderMutation{
SetHeaders: []*configPb.HeaderValueOption{
{
Header: &configPb.HeaderValue{
Key: header,
RawValue: []byte(headerValue),
},
},
},
},
},
},
},
}
log.Printf("Sending response: %+v\n", resp)
if err := srv.Send(resp); err != nil {
return status.Errorf(codes.Unknown, "error sending response: %v", err)
}
} else {
log.Printf("Not sending response %+v\n", resp)
continue
}
}
}
func main() {
// 1. 定义命令行参数
// 参数:port (int), headerSuffix (string)
port := flag.Int("port", 9000, "gRPC server port")
headerSuffix := flag.String("headerSuffix", "", "Suffix to append to x-extproc-hello header")
// 2. 解析参数
flag.Parse()
// 3. 监听指定端口
address := fmt.Sprintf(":%d", *port)
lis, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("Failed to listen on %s: %v", address, err)
}
grpcServer := grpc.NewServer()
// 4. 注册服务,并传入 headerSuffix
extProcPb.RegisterExternalProcessorServer(grpcServer, &extProcServer{
headerSuffix: *headerSuffix,
})
log.Printf("Starting gRPC server on %s with suffix: '%s'...\n", address, *headerSuffix)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}修改后的代码如上所示。我门配置了一个参数headerSuffix,根据headerSuffix,我们选择性的改变header的key。下面我们使用go run main.go -port 9002 -headerSuffix required和go run main.go -port 9000启动两个外部后端实例。
分别测试当``为true和false的情况。我们发现true的时候出现两个header,false的时候只出现一个,非required的那个filter被跳过了。
zheyu@ZhedeAir envoyext_proc % curl -I http://localhost:8082 -H "x-run-ext: true"
HTTP/1.1 200 OK
accept-ranges: bytes
age: 9500
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; hit
content-length: 15991
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Wed, 29 Apr 2026 03:11:17 GMT
etag: "7ffdbb8473232c16f9299e0ceaf9b2cf-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01KQBKGCW1DW3EV38B49BDBSB4
x-envoy-upstream-service-time: 138
x-extproc-hello-required: Hello from ext_proc
x-extproc-hello-: Hello from ext_proczheyu@ZhedeAir envoyext_proc % curl -I http://localhost:8082 -H "x-run-ext: false"
HTTP/1.1 200 OK
accept-ranges: bytes
age: 9677
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; hit
content-length: 15991
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Wed, 29 Apr 2026 03:14:14 GMT
etag: "7ffdbb8473232c16f9299e0ceaf9b2cf-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01KQBKNSP7ABDQZD9B4RZ0F9JQ
x-envoy-upstream-service-time: 112
x-extproc-hello-required: Hello from ext_proc基于EtensionWithMatcher
ExtensionWithmatcher相当于给一个filter加上一个wrapper,该wrapper给base filter添加判断逻辑,假如满足就执行,否则可以选择skip。
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 8082
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log:
- name: envoy.access_loggers.stdout
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% \"%RESP(X-EXTPROC-HELLO)%\" \"%RESP(CONTENT-TYPE)%\" \"%RESP(CONTENT-LENGTH)%\" %DURATION% ms\n"
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: [ "*" ]
routes:
- match:
prefix: "/"
route:
host_rewrite_literal: www.envoyproxy.io
cluster: service_envoyproxy_io
http_filters:
- name: with-matcher
typed_config:
"@type": type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher
extension_config:
name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
grpc_service:
envoy_grpc:
cluster_name: ext_proc_cluster
failure_mode_allow: true
processing_mode:
request_header_mode: SKIP
response_header_mode: SEND
xds_matcher:
matcher_list:
matchers:
- predicate:
single_predicate:
input:
name: request-headers
typed_config:
"@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput
header_name: x-run-ext
value_match:
exact: "false"
on_match:
action:
name: skip
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: ext_proc_cluster
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: ext_proc_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 9000
- name: service_envoyproxy_io
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
load_assignment:
cluster_name: service_envoyproxy_io
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: www.envoyproxy.io
port_value: 443
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
sni: www.envoyproxy.io如下示例,我们给刚才的ext_proc添加一个ExtentionWithMatcher。该filter加了一个判断逻辑,即假如headerx-run-ext是false,则跳过ext_proc filter。下面我们来观察实验效果。
当x-run-ext不是false,我们看到了x-extproc-hello- header。
zheyu@ZhedeAir envoyext_proc % curl -I http://localhost:8082 -H "x-run-ext: 234"
HTTP/1.1 200 OK
accept-ranges: bytes
age: 4092
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; hit
content-length: 15991
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Thu, 30 Apr 2026 01:12:11 GMT
etag: "114158b78a6492cac9dd8fb00e036d76-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01KQDZ310WTPCBRRWTDZYMFD7Z
x-envoy-upstream-service-time: 107
x-extproc-hello-: Hello from ext_proc当x-run-ext是false,我们看不到x-extproc-hello- header。
zheyu@ZhedeAir envoyext_proc % curl -I http://localhost:8082 -H "x-run-ext: false"
HTTP/1.1 200 OK
accept-ranges: bytes
age: 5218
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; hit
content-length: 15991
content-security-policy: frame-ancestors 'self';
content-type: text/html; charset=UTF-8
date: Thu, 30 Apr 2026 01:30:57 GMT
etag: "114158b78a6492cac9dd8fb00e036d76-ssl"
server: envoy
strict-transport-security: max-age=31536000
x-nf-request-id: 01KQE05CFGRY7NKP97A63Y3Q0K
x-envoy-upstream-service-time: 110