WEBKT

Envoy 实战:用 RE2:Set 打造高性能 WAF 过滤器

200 0 0 0

各位老铁,大家好!我是你们的赛博朋克老司机,极客君。

今天咱们来聊点硬核的,聊聊怎么用 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 会:

  1. 加载 WAF 规则: 从配置文件或者 xDS API 中加载 WAF 规则(这些规则是基于 RE2:Set 的)。
  2. 编译规则: 将加载的规则编译成 RE2:Set 对象。
  3. 匹配请求: 对每个进入 Envoy 的请求,提取关键信息(如 URL、Header、Body 等),使用 RE2:Set 进行匹配。
  4. 执行动作: 如果匹配到规则,则执行相应的动作(如拒绝请求、记录日志、重定向等)。

整个流程如下图所示:

[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,如果你有任何问题或者建议,欢迎留言讨论!

好了,今天的分享就到这里。我是极客君,咱们下期再见!

极客君 EnvoyWAFRE2

评论点评