Envoy 实战:用 RE2:Set 打造高性能 WAF 过滤器
各位老铁,大家好!我是你们的赛博朋克老司机,极客君。
今天咱们来聊点硬核的,聊聊怎么用 Envoy 打造一个性能炸裂的 WAF(Web Application Firewall)。相信不少做过网站或者搞过服务器的兄弟都对 WAF 不陌生,这玩意儿能帮你挡住各种奇奇怪怪的攻击,保护你的应用安全。
但传统的 WAF,要么规则配置麻烦,要么性能堪忧。今天,我就来给大家分享一个基于 Envoy 和 RE2:Set 的高性能 WAF 解决方案,让你鱼和熊掌兼得!
为什么选择 Envoy 和 RE2:Set?
在深入讲解之前,咱们先来聊聊为什么选择 Envoy 和 RE2:Set 这对组合。
Envoy:云原生时代的流量管理大师
Envoy,这名字听起来就透着一股子“特使”范儿。没错,它就是云原生时代的流量管理特使,由 Lyft 开源,现在是 CNCF(云原生计算基金会)的毕业项目。Envoy 的优点简直不要太多:
- 高性能: 基于 C++11/14 开发,性能杠杠的。
- 可扩展性: 模块化设计,你可以像搭积木一样,通过过滤器(Filter)链机制扩展 Envoy 的功能。
- 动态配置: 支持 xDS API,可以动态更新配置,无需重启 Envoy。
- 可观测性: 提供了丰富的 metrics、tracing 和 logging,方便你监控和调试。
总之,Envoy 就是一个为云原生而生的流量管理利器,用来做 WAF 再合适不过了。
RE2:Set:高效的正则表达式引擎
RE2 是 Google 出品的一个正则表达式引擎,它的特点是:
- 安全: 不会因为恶意的正则表达式而导致 ReDoS(正则表达式拒绝服务)攻击。
- 高效: 使用有限状态自动机(DFA)实现,匹配速度快,且不受正则表达式复杂度的影响。
- RE2:Set 支持: 允许你将多个正则表达式编译成一个集合,一次匹配多个规则,进一步提高效率。
对于 WAF 来说,需要处理大量的规则,RE2:Set 的高效性和安全性就显得尤为重要。
核心思路:Envoy Filter 链 + RE2:Set
我们的核心思路很简单,就是利用 Envoy 的 Filter 链机制,将 RE2:Set 编译的 WAF 规则集成到 Envoy 的请求处理流程中。具体来说,我们会创建一个自定义的 Envoy Filter,这个 Filter 会:
- 加载 WAF 规则: 从配置文件或者 xDS API 中加载 WAF 规则(这些规则是基于 RE2:Set 的)。
- 编译规则: 将加载的规则编译成 RE2:Set 对象。
- 匹配请求: 对每个进入 Envoy 的请求,提取关键信息(如 URL、Header、Body 等),使用 RE2:Set 进行匹配。
- 执行动作: 如果匹配到规则,则执行相应的动作(如拒绝请求、记录日志、重定向等)。
整个流程如下图所示:
[Client Request] --> [Envoy Listener] --> [HTTP Filter Chain] --> [Custom WAF Filter (RE2:Set)] --> [Router Filter] --> [Upstream Server]
| ^
| |
| (Match Rules & Take Actions) |
| |
V |
[Reject/Log/Redirect...] <------------------
实战步骤:手把手教你搭建 WAF
理论说了这么多,接下来咱们就真刀真枪地干起来!
1. 准备工作
- 安装 Envoy: 可以参考 Envoy 官方文档进行安装。
- 安装 RE2: 可以通过包管理器(如 apt、yum、brew 等)安装,也可以从源码编译安装。
- 安装 Bazel: 如果你要从源码编译 Envoy Filter,需要安装 Bazel 构建工具。
2. 编写 WAF 规则
WAF 规则通常是一些正则表达式,用于匹配恶意请求的特征。例如,下面是一些常见的 WAF 规则:
# 防止 SQL 注入
.*(?:;|\/\*|\*\/|\-\-).*\b(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|UNION|EXEC|DECLARE)\b.*
# 防止 XSS 攻击
.*<script.*?>.*</script>.*
.*<.*on(?:load|click|mouseover|...).*?>.*
# 防止路径遍历
.*\.\.\/.*
# 防止命令注入
.*;\s*(?:cat|ls|whoami|id|pwd).*\n```
你可以根据自己的需求编写 WAF 规则,并将它们保存到一个文件中(例如 `waf_rules.txt`)。
### 3. 创建自定义 Envoy Filter
接下来,我们需要创建一个自定义的 Envoy Filter,用于加载和匹配 WAF 规则。这里我们使用 C++ 来编写 Filter。
#### 3.1 创建 Filter 代码
```c++
// waf_filter.h
#pragma once
#include "envoy/http/filter.h"
#include "envoy/registry/registry.h"
#include "re2/re2.h"
#include "re2/set.h"
namespace Envoy {
namespace Http {
class WafFilter : public Filter {
public:
WafFilter(const std::string& rules_file);
~WafFilter() override;
FilterHeadersStatus decodeHeaders(RequestHeaderMap& headers, bool end_stream) override;
FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override;
FilterTrailersStatus decodeTrailers(RequestTrailerMap& trailers) override;
private:
re2::RE2::Set rule_set_;
};
} // namespace Http
} // namespace Envoy
// waf_filter.cc
#include "waf_filter.h"
#include <fstream>
#include <iostream>
namespace Envoy {
namespace Http {
WafFilter::WafFilter(const std::string& rules_file) : rule_set_(re2::RE2::Options(), re2::RE2::Anchor::UNANCHORED) {
std::ifstream file(rules_file);
std::string line;
if (file.is_open()) {
while (std::getline(file, line)) {
if (!line.empty() && line[0] != '#') { // 忽略注释
rule_set_.Add(line, nullptr);
}
}
file.close();
if (int error_code = rule_set_.Compile()) {
//处理错误
std::cerr <<"rule_set_.Compile() failed with: " << error_code << "\n";
throw std::runtime_error("Failed to compile WAF rules");
}
} else {
//处理错误
throw std::runtime_error("Failed to open WAF rules file");
}
}
WafFilter::~WafFilter() {}
FilterHeadersStatus WafFilter::decodeHeaders(RequestHeaderMap& headers, bool end_stream) {
// 匹配请求头
for (const auto& header : headers) {
if (rule_set_.Match(header.key()->value().getStringView(), nullptr)) {
// 执行拦截
headers.setStatus(403);
return FilterHeadersStatus::StopIteration;
}
if (rule_set_.Match(header.value()->value().getStringView(), nullptr)) {
// 执行拦截
headers.setStatus(403);
return FilterHeadersStatus::StopIteration;
}
}
return FilterHeadersStatus::Continue;
}
FilterDataStatus WafFilter::decodeData(Buffer::Instance& data, bool end_stream) {
// 匹配请求体(可选)
if (rule_set_.Match(data.toString(), nullptr)){
//这里应该返回 FilterDataStatus::StopIterationAndBuffer 或者 FilterDataStatus::StopIterationNoBuffer
// 并且在 decodeHeaders 里面设置 403 状态
}
return FilterDataStatus::Continue;
}
FilterTrailersStatus WafFilter::decodeTrailers(RequestTrailerMap& trailers) {
// 匹配请求尾部(可选)
return FilterTrailersStatus::Continue;
}
} // namespace Http
} // namespace Envoy
3.2 注册 Filter
// waf_filter_config.cc
#include "envoy/config/filter/http/http_filter_config.h"
#include "envoy/registry/registry.h"
#include "waf_filter.h"
namespace Envoy {
namespace Server {
namespace Configuration {
class WafFilterConfig : public NamedHttpFilterConfigFactory {
public:
HttpFilterFactoryCb createFilterFactoryFromProto(
const Protobuf::Message& config, const std::string&, FactoryContext& context) override {
// 从配置中获取 WAF 规则文件路径 (示例)
auto& cfg = MessageUtil::downcastAndValidate<const mywaf::WafConfig&>(config, context.messageValidationVisitor());
const std::string rules_file = cfg.rules_file();
return [rules_file](HttpFilterManager& filter_manager) -> void {
filter_manager.addFilter(std::make_shared<Http::WafFilter>(rules_file));
};
}
ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return ProtobufTypes::MessagePtr{new mywaf::WafConfig()};
}
std::string name() const override { return "my.http.waf"; }
};
static Registry::RegisterFactory<WafFilterConfig, NamedHttpFilterConfigFactory> register_;
} // namespace Configuration
} // namespace Server
} // namespace Envoy
// waf.proto
syntax = "proto3";
package mywaf;
message WafConfig {
string rules_file = 1;
}
3.3 编译 Filter
使用 Bazel 编译 Filter:
bazel build //path/to/your/filter:waf_filter.so
4. 配置 Envoy
最后,我们需要配置 Envoy,加载我们的自定义 Filter,并指定 WAF 规则文件。
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 8080
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
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: some_upstream_cluster
http_filters:
- name: my.http.waf # 我们的自定义 Filter
typed_config:
"@type": type.googleapis.com/mywaf.WafConfig
rules_file: /path/to/your/waf_rules.txt # WAF 规则文件路径
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: some_upstream_cluster
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: some_upstream_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 8081
将 waf_filter.so 文件放到Envoy可以加载的路径.
5. 测试
启动 Envoy,然后发送一些恶意请求进行测试。如果一切正常,Envoy 应该会拦截这些请求,并返回 403 状态码。
总结
通过 Envoy 的 Filter 链机制和 RE2:Set,我们可以轻松打造一个高性能、可扩展的 WAF。这只是一个简单的示例,你可以根据自己的需求进行更复杂的定制,例如:
- 支持更多的匹配条件: 除了 URL、Header、Body,还可以匹配 Cookie、IP 地址等。
- 更灵活的动作: 除了拒绝请求,还可以重定向、添加 Header、修改 Body 等。
- 集成 xDS API: 从 xDS API 动态获取 WAF 规则,实现动态更新。
- 更完善的日志记录: 记录详细的 WAF 日志,方便分析和排查问题。
希望这篇文章能帮助你更好地理解 Envoy 和 WAF,如果你有任何问题或者建议,欢迎留言讨论!
好了,今天的分享就到这里。我是极客君,咱们下期再见!