diff --git a/.agents/threat_model/THREAT_MODEL.md b/.agents/threat_model/THREAT_MODEL.md new file mode 100644 index 0000000000000..6c4aeb4dde521 --- /dev/null +++ b/.agents/threat_model/THREAT_MODEL.md @@ -0,0 +1,121 @@ +# Threat Model: Envoy Proxy + +## 1. System context + +Envoy is a CNCF-graduated L3/L4 and L7 proxy written in modern C++ (with growing Rust components), built with Bazel, and deployed as an **edge/front proxy** terminating untrusted Internet traffic and as a **service-mesh sidecar** alongside every workload in a microservices fleet (Istio, Cilium, AWS App Mesh, Consul Connect). It speaks HTTP/1.1, HTTP/2, HTTP/3-QUIC, gRPC, **MCP (Model Context Protocol), A2A (Agent-to-Agent)**, and a long tail of L7 protocols (Redis, Thrift, DNS, …) via a filter-chain extension model. The MCP filters (`mcp`, `mcp_router`, `mcp_json_rest_bridge`, `mcp_multicluster`) make Envoy an AI-agent-protocol gateway: aggregating tool lists from multiple MCP backends and routing `tools/call` requests. Configuration is delivered statically via bootstrap YAML or dynamically via the gRPC/REST **xDS** APIs. The `mobile/` subtree ships Envoy as a client-side networking library for iOS/Android. + +This model covers the **upstream envoyproxy/envoy project** as shipped: core, Envoy Mobile, the CI/release pipeline, and — from the alpha tier — the MCP and A2A extensions only. `contrib/`, all other alpha-status extensions, and Windows-specific code paths are out of scope (§5). + +The project publishes its own threat model (`docs/root/intro/arch_overview/security/threat_model.rst`), which this document incorporates with **one owner-directed divergence**: ext_authz and ext_proc side-call responses are treated as **untrusted** here (upstream currently declares side-calls trusted). Other side-calls (rate-limit, ALS, tracers, credential suppliers) remain trusted. Upstream's other assumptions are adopted: downstream and upstream peers are untrusted for core; xDS transport is trusted; Lua/Wasm/dynamic-module code is trusted; the admin interface is operator-only; DoS triggers the security process only at ≥100× amplification or query-of-death on hardened components. + +The codebase is ~3k C++ source files plus ~113 vendored dependencies (BoringSSL, nghttp2, quiche, c-ares, V8, LuaJIT, wasmtime, RE2, abseil, …). It is continuously fuzzed via OSS-Fuzz and has shipped ~80+ first-party CVEs/GHSAs since 2019, dominated by HTTP/2 frame-handling DoS, HTTP filter-chain lifecycle use-after-free, and authn/authz filter bypass. + +## 2. Assets + +| asset | description | sensitivity | +|---|---|---| +| Process & host integrity | Native C++ parsing untrusted wire bytes; memory corruption → RCE in a process with network reach into the entire mesh/edge | critical | +| TLS private keys & session tickets | Server/client cert private keys and TLS session-ticket keys held in-process by SDS/secret manager; leak = passive decrypt + edge impersonation | critical | +| Proxied request/response data | Decrypted bodies, headers, cookies, bearer tokens for every request after TLS termination | critical | +| Service availability | Envoy is the front door (edge) and the only data-path hop (mesh); a parser crash or OOM is a full outage | critical | +| Mobile app process & on-device user data | Envoy Mobile runs inside iOS/Android apps; compromise = app-privilege code exec and access to on-device user data | critical | +| Internal upstream services | Route table + cluster manager give Envoy authenticated reach to every backend, metadata server, and control plane (SSRF pivot) | high | +| xDS configuration integrity | Routes, listeners, clusters, RBAC, filter bytecode delivered via xDS; integrity loss = arbitrary traffic redirection | high | +| mTLS workload identity | SPIFFE SVID validation result; downstream RBAC and upstreams trust it | high | +| AuthN/AuthZ verdicts | jwt_authn claims, ext_authz allow/deny, RBAC decisions propagated as trusted `x-envoy-*` / dynamic metadata | high | +| Injected upstream credentials | OAuth2 client_secret + HMAC key, AWS SigV4 keys, credential-injector secrets attached to outbound requests | high | +| Agent tool-call & capability integrity | MCP/A2A `tools/call` payloads, capability negotiations, and aggregated tool lists that downstream agents act on; tampering = arbitrary tool execution | high | +| Release artifact integrity | `docker.io/envoyproxy/*` images, signed deb/rpm/tarballs, Maven Central / PyPI envoy-mobile packages consumed by the entire ecosystem | high | +| CI/release secrets | GPG maintainer signing key, DockerHub creds, GCP SA keys, Sonatype creds, multiple GitHub App private keys with multi-repo write | high | +| Access logs & telemetry | Request lines, headers, JWT subjects, client IPs to file/gRPC/OTel sinks; PII-bearing and an integrity target (log injection) | medium | +| Internal trust headers | `x-envoy-*`, XFF, `x-request-id` that upstreams treat as authoritative; failure to strip = trust-boundary bypass | medium | + +## 3. Entry points & trust boundaries + +| entry_point | description | trust_boundary | reachable_assets | +|---|---|---|---| +| Downstream TCP listener | Socket accept → listener-filter chain → network-filter `onData`; first touch of untrusted bytes (`source/common/listener_manager/active_tcp_listener.cc`) | untrusted Internet client → worker thread | Process & host integrity, Service availability, Proxied request/response data | +| Downstream UDP/QUIC ingest | UDP `recvmmsg` → quiche dispatcher; pre-handshake, source-spoofable (`source/common/quic/active_quic_listener.cc`) | spoofable Internet UDP → process memory | Process & host integrity, Service availability | +| HTTP/1 codec | Balsa / http-parser tokenises request-line, headers, chunked TE (`source/common/http/http1/codec_impl.cc`) | untrusted downstream bytes → HCM | Process & host integrity, Proxied request/response data, AuthN/AuthZ verdicts, Internal trust headers | +| HTTP/2 codec | nghttp2/oghttp2 HPACK decode, stream mux, flood limits, METADATA (`source/common/http/http2/codec_impl.cc`) | untrusted downstream bytes → HCM | Process & host integrity, Service availability, Proxied request/response data | +| HTTP/3/QUIC codec | quiche QPACK + H3 frame parsing, CONNECT-UDP datagrams (`source/common/quic/envoy_quic_server_stream.cc`) | untrusted downstream bytes → HCM | Process & host integrity, Service availability | +| Listener filters (TLS-inspector / PROXY-protocol) | Hand-rolled parsers peek raw bytes pre-filter-chain to extract SNI/ALPN or PROXY v1/v2 TLVs (`source/extensions/filters/listener/{tls_inspector,proxy_protocol}/`) | untrusted downstream bytes → filter-chain selection | Process & host integrity, Internal trust headers, AuthN/AuthZ verdicts | +| TLS handshake & cert validation | BoringSSL handshake; client-cert chain verify, SAN/SPKI match, OCSP ASN.1 parse (`source/common/tls/cert_validator/`) | untrusted peer X.509 → trust decision | mTLS workload identity, TLS private keys & session tickets, Process & host integrity | +| Header & path normalization | Header storage/validation, `:path` canonicalisation, `%2F`/slash-merge (`source/common/http/path_utility.cc`, `header_utility.cc`) | attacker-controlled headers → security decisions | AuthN/AuthZ verdicts, Internal upstream services, Internal trust headers | +| Route matching & regex | VirtualHost/route eval; tenant-supplied RE2 patterns evaluated on attacker input (`source/common/router/config_impl.cc`, `source/common/common/regex.cc`) | untrusted headers × semi-trusted config → routing/auth | Internal upstream services, AuthN/AuthZ verdicts, Service availability | +| Non-HTTP L7 codecs (core) | Hand-written wire decoders: Redis, Thrift, Dubbo, Mongo, ZooKeeper (`source/extensions/filters/network/*/decoder*.cc`) | untrusted downstream bytes → proxy logic | Process & host integrity, Service availability | +| DNS UDP filter (server) | Envoy-as-DNS-server parses arbitrary queries (`source/extensions/filters/udp/dns_filter/dns_parser.cc`) | spoofable UDP → DNS parser | Process & host integrity, Service availability | +| MCP / A2A agent-protocol filters | JSON-RPC request body parsing + multi-backend response aggregation, session routing (`source/extensions/filters/http/{mcp,mcp_router,a2a}/`, `mcp_json_parser.cc` 23K, `mcp_router.cc` 60K) | untrusted downstream agent ↔ untrusted upstream MCP/A2A server | Process & host integrity, Agent tool-call & capability integrity, Internal upstream services, Service availability | +| xDS control-plane ingestion | gRPC/REST/filesystem `DiscoveryResponse` → proto unpack → live reconfig (`source/extensions/config_subscription/grpc/grpc_mux_impl.cc`) | management server (trusted transport, semi-trusted payload) → data-plane config | xDS configuration integrity, Service availability, Process & host integrity | +| Upstream response path | Envoy-as-client parses untrusted upstream HTTP/1/2/3 responses (`source/common/router/upstream_request.cc`, `source/common/http/codec_client.cc`) | malicious/compromised upstream → process memory & downstream | Process & host integrity, Proxied request/response data, Service availability | +| ext_proc / ext_authz responses | Side-call gRPC returns header/body mutations or allow/deny that Envoy applies verbatim (`source/extensions/filters/http/ext_proc/mutation_utils.cc`, `source/extensions/filters/common/ext_authz/`) | **untrusted** side-call service → request mutation & filter-chain state | AuthN/AuthZ verdicts, Proxied request/response data, Internal trust headers, Process & host integrity | +| Decompression & transcoding | gzip/brotli/zstd inflate of attacker bodies; gRPC↔JSON transcoder (`source/extensions/compression/*/decompressor/`, `source/extensions/filters/http/grpc_json_transcoder/`) | untrusted body bytes → heap | Service availability, Process & host integrity | +| JWT / OAuth2 authn filters | Parse attacker-supplied JWT (b64/JSON/sig), OAuth2 redirect/state/cookies, fetch JWKS (`source/extensions/filters/http/{jwt_authn,oauth2}/`) | untrusted credential bytes → auth decision | AuthN/AuthZ verdicts, Injected upstream credentials, Internal upstream services | +| Access-log formatter | `%REQ()%` / `%RESP()%` / `%CEL()%` interpolate attacker headers into log sinks (`source/common/formatter/substitution_formatter.cc`) | untrusted headers → log sinks / SIEM | Access logs & telemetry | +| DNS resolver (client) | c-ares / getaddrinfo / Apple / hickory parse responses from upstream resolver (`source/extensions/network/dns_resolver/cares/dns_impl.cc`) | untrusted DNS responder → cluster endpoints | Process & host integrity, Internal upstream services | +| Envoy Mobile upstream ingest | Envoy Mobile (iOS/Android client library) parses responses from arbitrary servers; trust model inverted — server is the attacker (`mobile/library/common/`) | malicious origin server → mobile app process | Mobile app process & on-device user data, Proxied request/response data | +| Bazel dependency fetch | ~113 `http_archive` deps fetched at build, SHA256-pinned (`bazel/repository_locations.bzl`) | upstream maintainer / mirror → compiled binary | Release artifact integrity, Process & host integrity | +| GitHub Actions CI pipeline | `pull_request_target` → `workflow_run` chain executes PR code; `trusted` flag computed in `_load.yml` gates secrets (`.github/workflows/_run.yml`) | external contributor → CI runner with org secrets | CI/release secrets, Release artifact integrity | +| Release artifact publish | Docker push (no cosign, `--provenance=false`), GPG-signed tarballs, Sonatype/PyPI upload; PyPI action branch-pinned (`distribution/docker/build.sh`, `.github/workflows/mobile-release.yml`) | CI runner → public registries | Release artifact integrity | + +## 4. Threats + +| id | threat | actor | surface | asset | impact | likelihood | status | controls | evidence | +|---|---|---|---|---|---|---|---|---|---| +| T1 | Memory corruption (use-after-free) leading to RCE via stream-reset / async-callback ordering races in the HTTP filter chain | remote_unauth | HTTP/1 codec, HTTP/2 codec, HTTP/3/QUIC codec, Upstream response path | Process & host integrity | critical | almost_certain | partially_mitigated | OSS-Fuzz, ASAN CI, deferred-deletion idiom | CVE-2026-26311, CVE-2026-26330, CVE-2024-45810, CVE-2023-35943, CVE-2023-35942, CVE-2022-29227, 7be853f757 | +| T2 | Authentication / authorization bypass via auth-filter logic or header-handling flaws | remote_unauth | JWT / OAuth2 authn filters, Header & path normalization, ext_proc / ext_authz responses, Listener filters (TLS-inspector / PROXY-protocol) | AuthN/AuthZ verdicts, Internal upstream services | critical | almost_certain | partially_mitigated | header sanitization in HCM, RBAC filter | CVE-2022-29226, CVE-2023-35941, CVE-2026-26308, CVE-2020-25017, CVE-2024-45806, CVE-2025-55162, CVE-2024-23322 | +| T3 | Memory corruption leading to RCE via hand-written non-HTTP L7 protocol parsers | remote_unauth | Non-HTTP L7 codecs (core), DNS UDP filter (server), Listener filters (TLS-inspector / PROXY-protocol) | Process & host integrity | critical | likely | partially_mitigated | per-extension `security_posture` tag; OSS-Fuzz on subset | CVE-2024-23322, CVE-2024-23323, CVE-2024-23324, CVE-2024-23325, CVE-2026-26310, CVE-2019-18801 | +| T4 | Memory corruption or crash via crafted HTTP/3 / QUIC stream-state transitions | remote_unauth | Downstream UDP/QUIC ingest, HTTP/3/QUIC codec | Process & host integrity, Service availability | critical | likely | partially_mitigated | quiche owned/fuzzed by Google; QUIC behind feature flag in many deploys | CVE-2024-32974, CVE-2024-32976, CVE-2024-34362 | +| T29 | Agent tool-call tampering, session confusion, or memory corruption via MCP/A2A JSON-RPC parsing and router response construction | remote_unauth | MCP / A2A agent-protocol filters | Process & host integrity, Agent tool-call & capability integrity, Internal upstream services | critical | likely | unmitigated | alpha tag only; no fuzz target | | +| T5 | mTLS / TLS identity spoofing via certificate-validation edge cases | remote_unauth | TLS handshake & cert validation | mTLS workload identity, Internal upstream services | critical | possible | partially_mitigated | BoringSSL; SPIFFE validator; SAN matchers | CVE-2025-66220, CVE-2023-0286 | +| T26 | Memory corruption or crash via malformed ext_proc / ext_authz gRPC response applied by `mutation_utils` | adjacent_network | ext_proc / ext_authz responses | Process & host integrity | critical | possible | unmitigated | none (path written assuming trusted input; no fuzz target) | | +| T28 | Mobile-app RCE or data exfiltration via malicious server response to Envoy Mobile client | remote_unauth | Envoy Mobile upstream ingest | Mobile app process & on-device user data | critical | possible | partially_mitigated | shares hardened core codecs; iOS/Android process sandbox | | +| T8 | CI secret exfiltration and signed-release takeover via `pull_request_target` trust-flag confusion or toolshed-action compromise | supply_chain | GitHub Actions CI pipeline | CI/release secrets, Release artifact integrity | critical | possible | partially_mitigated | actions SHA-pinned (one exception); `external-contributors` env gate; `trusted` flag in `_load.yml` | | +| T9 | Malicious release binary via EngFlow remote build-cache poisoning | supply_chain | GitHub Actions CI pipeline, Bazel dependency fetch | Release artifact integrity | critical | rare | partially_mitigated | EngFlow tenant ACL (out of tree); Bazel action hashing | | +| T11 | Request smuggling / cache poisoning via HTTP/1 parser differential between Envoy and upstream | remote_unauth | HTTP/1 codec, Upstream response path, Header & path normalization | Proxied request/response data, AuthN/AuthZ verdicts, Internal upstream services | high | almost_certain | partially_mitigated | Balsa parser; strict header checks (operator-configurable) | CVE-2019-9900, CVE-2019-18802, CVE-2024-45809, CVE-2024-34363, CVE-2023-35944, CVE-2025-64763, CVE-2020-25018 | +| T10 | Resource exhaustion (CPU/memory) via crafted HTTP/2 frame sequences | remote_unauth | HTTP/2 codec, Downstream TCP listener | Service availability | high | almost_certain | partially_mitigated | `protocol_constraints.cc` flood limits; overload manager (operator-configured, off by default) | CVE-2024-30255, CVE-2023-44487, CVE-2020-11080, CVE-2020-12603, CVE-2020-12604, CVE-2020-12605, CVE-2023-35945, CVE-2026-27135 | +| T12 | Route / RBAC policy bypass and SSRF to internal services via path-normalization or matcher edge cases | remote_unauth | Header & path normalization, Route matching & regex | Internal upstream services, AuthN/AuthZ verdicts | high | likely | partially_mitigated | `normalize_path`, `merge_slashes`, `path_with_escaped_slashes_action` (operator-configured) | CVE-2019-9901, CVE-2023-27487, CVE-2020-25017 | +| T13 | Memory / CPU exhaustion via decompression bomb or transcoder amplification | remote_unauth | Decompression & transcoding | Service availability | high | likely | partially_mitigated | per-decompressor output-size limits (added post-CVE); buffer watermarks | CVE-2022-29225, CVE-2024-32475 | +| T14 | Memory corruption or cluster-endpoint poisoning via malicious DNS responses | adjacent_network | DNS resolver (client) | Process & host integrity, Internal upstream services | high | likely | partially_mitigated | c-ares maintained upstream; hickory (Rust) alternative | GHSA-fg9g-pvc4-776f, CVE-2025-31498, CVE-2023-32067, CVE-2023-31147, GHSA-g9vw-6pvx-7gmw | +| T15 | Crash, memory corruption, or response smuggling via malicious upstream responses | remote_auth | Upstream response path | Process & host integrity, Proxied request/response data, Service availability | high | likely | partially_mitigated | core declared robust-to-untrusted-upstream; same codec hardening as downstream | CVE-2022-29224, CVE-2023-35945, CVE-2024-45809, CVE-2024-34364 | +| T18 | Request/response tampering and auth-verdict forgery via untrusted ext_proc / ext_authz side-call | adjacent_network | ext_proc / ext_authz responses | Proxied request/response data, AuthN/AuthZ verdicts, Internal trust headers | high | likely | partially_mitigated | `mutation_rules` allowlist (opt-in); `allowed_headers`/`disallowed_headers` on ext_authz (opt-in) | | +| T27 | Resource exhaustion via unbounded ext_proc body mutation, header-set size, or stream hold-open | adjacent_network | ext_proc / ext_authz responses | Service availability | high | likely | partially_mitigated | `message_timeout`; per-message size limits (partial) | | +| T16 | Fleet-wide crash or resource exhaustion via malformed / pathological xDS configuration | insider | xDS control-plane ingestion, Route matching & regex | Service availability, xDS configuration integrity | high | likely | partially_mitigated | PGV proto validation; config-rejection stats; RE2 (linear-time) for regex | a20c0ab8dd, 2bcebccc0d, CVE-2019-15225, CVE-2019-15226 | +| T17 | Downstream consumer compromise via tampered OCI image (no signature / no SLSA provenance) | supply_chain | Release artifact publish | Release artifact integrity | high | possible | unmitigated | GPG on tarballs only; `--sbom=false --provenance=false` set; no cosign | | +| T19 | Traffic redirection or filter-chain injection via compromised xDS management server | insider | xDS control-plane ingestion | xDS configuration integrity, Proxied request/response data, Internal upstream services | high | possible | risk_accepted | upstream threat model declares xDS transport trusted; mTLS to control plane | | +| T20 | Build-time code injection via compromised upstream dependency tarball | supply_chain | Bazel dependency fetch | Release artifact integrity, Process & host integrity | high | rare | mitigated | all 113 deps SHA256-pinned; `tools/dependency/` validators; CPE tracking | | +| T21 | Log injection / SIEM poisoning via unescaped attacker-controlled fields in access-log output | remote_unauth | Access-log formatter | Access logs & telemetry | medium | likely | partially_mitigated | `json_format` escaping; `JsonEscaper` (itself patched for OOB) | CVE-2024-45808, CVE-2026-26309 | +| T22 | CPU exhaustion via tenant-supplied regex evaluated against attacker input (multi-tenant xDS) | remote_unauth | Route matching & regex, xDS control-plane ingestion | Service availability | medium | possible | partially_mitigated | RE2 default (linear); program-size limit; std::regex removed | CVE-2019-15225 | +| T25 | Reflection / amplification abuse of Envoy QUIC or DNS UDP listeners against third parties | remote_unauth | Downstream UDP/QUIC ingest, DNS UDP filter (server) | Service availability | low | possible | partially_mitigated | QUIC Retry / amplification limit in quiche; DNS filter response-size cap | | + +## 5. Deprioritized + +| threat | reason | +|---|---| +| Admin HTTP interface — process control / info disclosure | Admin endpoint is operator-trusted per upstream threat model; network exposure is operator deployment misconfiguration, not an Envoy bug. Surface removed from §3. | +| Wasm / Lua / dynamic-module host ABI — sandbox escape | Module code is trusted per upstream threat model; sandbox is explicitly not a security boundary. Runtime CVEs (CVE-2023-26489, CVE-2023-27477, CVE-2024-25176/77/78, CVE-2025-53901) are tracked as dependency hygiene, not modeled threats. Surface removed from §3. | +| CLI / env — privilege via process environment | Process spawner (orchestrator/operator) is trusted; no privilege boundary crossed. Surface removed from §3. | +| Hot-restart UDS — FD/state theft by same-netns process | Same-network-namespace local process is trusted; netns isolation is the boundary, not the abstract socket. Surface removed from §3. | +| Bootstrap & file-based config — arbitrary file read via config paths | Config author is trusted per upstream threat model. Surface removed from §3. | +| `contrib/` and alpha-status extensions (except MCP and A2A) | Outside upstream security-team coverage per `EXTENSION_POLICY.md`; owner confirms out of scope for this model. Includes Postgres/MySQL/Kafka/SIP/RocketMQ/Go-filter parsers. Surface removed from §3. | +| Windows-specific code paths | Out of scope for this model per owner. | +| Repudiation of proxied requests | Envoy is not the system of record for non-repudiation; access logs are an asset (§2), but cryptographic non-repudiation is out of scope. | +| Side-channel / timing attacks on TLS private keys | BoringSSL's responsibility; constant-time guarantees inherited. | +| DoS below 100× amplification on hardened components | Upstream explicitly does not treat sub-threshold DoS as a security issue; operators must configure overload manager. | + +## 6. Recommended mitigations + +| mitigation | threat_ids | closes_class | effort | +|---|---|---|---| +| Adopt a uniform deferred-stream-destruction / weak-ref idiom across all async filter callbacks; add a clang-tidy check for raw `this` capture in posted callbacks | T1 | partial | L | +| Strict-by-default HCM header validation (reject duplicate auth-relevant headers, reject CL+TE conflict, reject embedded NUL/CR/LF) independent of UHV | T2, T11 | partial | M | +| Make `normalize_path` / `merge_slashes` / `path_with_escaped_slashes_action: REJECT_REQUEST` the safe default | T12 | partial | S | +| Require `robust_to_untrusted_downstream` posture + a fuzz target before any L7 codec graduates from alpha | T3, T29 | partial | M | +| Ship overload-manager and HTTP/2 flood limits as on-by-default safe baseline rather than operator opt-in | T10, T13, T22 | partial | M | +| Make ext_proc `mutation_rules` deny-by-default; add a fuzz target for the `ProcessingResponse` / `CheckResponse` apply path | T18, T26, T27 | partial | M | +| Replace `StrCat` JSON construction in `mcp_router` with a structured serializer; add fuzz targets for `mcp_json_parser` / `a2a_json_parser` and the backend-response merge path | T29 | partial | M | +| Migrate default DNS resolver from c-ares to hickory (Rust) or getaddrinfo-behind-local-stub | T14 | partial | M | +| Sign OCI images with cosign and emit SLSA provenance; SHA-pin `pypa/gh-action-pypi-publish`; move GCP auth to WIF/OIDC | T8, T17 | partial | S | +| Isolate release builds from PR-triggered cache writes (separate EngFlow cache namespace or `--noremote_upload_local_results` for untrusted) | T9 | yes | S | +| Hard-cap decompressed output as a ratio of input across all decompressors and the gRPC-JSON transcoder | T13 | yes | S | +| Escape/validate all formatter-expanded fields against the sink's grammar (newline-strip for text, full JSON escape for `json_format`) | T21 | yes | S | diff --git a/.bazelignore b/.bazelignore index 096b3dab21a68..778192d108514 100644 --- a/.bazelignore +++ b/.bazelignore @@ -6,3 +6,4 @@ mobile tools/dev/src .project envoy-filter-example +target diff --git a/.bazelrc b/.bazelrc index 0340779e1ce24..d02cec5f95aad 100644 --- a/.bazelrc +++ b/.bazelrc @@ -20,6 +20,10 @@ startup --host_jvm_args="-DBAZEL_TRACK_SOURCE_DIRECTORIES=1" ############################################################################# common --noenable_bzlmod +common --enable_workspace +common --noincompatible_disallow_empty_glob +common --noincompatible_disallow_ctx_resolve_tools +common --legacy_external_runfiles fetch --color=yes run --color=yes @@ -32,6 +36,8 @@ build --java_runtime_version=remotejdk_11 build --tool_java_runtime_version=remotejdk_11 build --java_language_version=11 build --tool_java_language_version=11 +# TODO(jwendell): Remove after https://github.com/protocolbuffers/protobuf/issues/20760 is fixed. +build --extra_toolchains=@envoy//bazel:envoy_java_toolchain_definition # silence absl logspam. build --copt=-DABSL_MIN_LOG_LEVEL=4 # Global C++ standard and common warning suppressions @@ -59,7 +65,7 @@ test --test_verbose_timeout_warnings test --experimental_ui_max_stdouterr_bytes=11712829 #default 1048576 # Allow tags to influence execution requirements -common --experimental_allow_tags_propagation +common --incompatible_allow_tags_propagation # Python common --@rules_python//python/config_settings:bootstrap_impl=script @@ -143,7 +149,6 @@ build:gcc --cxxopt=-Wno-nonnull-compare build:gcc --cxxopt=-Wno-trigraphs build:gcc --linkopt=-fuse-ld=gold --host_linkopt=-fuse-ld=gold build:gcc --host_platform=@envoy//bazel/rbe/toolchains:rbe_linux_gcc_platform -build:gcc --linkopt=-fuse-ld=gold --host_linkopt=-fuse-ld=gold build:gcc --action_env=BAZEL_LINKOPTS=-lm:-fuse-ld=gold # libc++ - default for clang @@ -159,6 +164,7 @@ common:libc++ --@envoy//bazel:libc++=true build:libstdc++ --action_env=BAZEL_LINKLIBS=-l%:libstdc++.a build:libstdc++ --@envoy//bazel:libc++=false build:libstdc++ --@envoy//bazel:libstdc++=true +build:libstdc++ --repo_env=BAZEL_USE_LIBSTDCPP=True ############################################################################# @@ -236,8 +242,9 @@ common:aws-lc-fips --@envoy//bazel:crypto=@aws_lc//:crypto common:aws-lc-fips --@envoy//bazel:http3=False # OpenSSL -common:openssl --//bazel:ssl=//compat/openssl:ssl -common:openssl --//bazel:crypto=//compat/openssl:crypto +common:openssl --@envoy//bazel:openssl=True +common:openssl --@envoy//bazel:ssl=@envoy//compat/openssl:ssl +common:openssl --@envoy//bazel:crypto=@envoy//compat/openssl:crypto common:openssl --copt=-DENVOY_SSL_OPENSSL common:openssl --host_copt=-DENVOY_SSL_OPENSSL common:openssl --@envoy//bazel:http3=False @@ -354,7 +361,7 @@ build:fuzz-coverage --test_tag_filters=-nocoverage # resources required to build and run the tests. build:fuzz-coverage --define=wasm=disabled build:fuzz-coverage --config=fuzz-coverage-config -build:fuzz-coverage-config --//tools/coverage:config=@envoy//test:fuzz_coverage_config +build:fuzz-coverage-config --@envoy//tools/coverage:config=@envoy//test:fuzz_coverage_config build:oss-fuzz --config=fuzzing build:oss-fuzz --config=libc++ @@ -402,7 +409,7 @@ build:clang-tidy --build_tag_filters=-notidy # Compile database generation config build:compdb --build_tag_filters=-nocompdb -common:cves --//tools/dependency:cve-data=//tools/dependency:cve-data-dir +common:cves --@envoy//tools/dependency:cve-data=@envoy//tools/dependency:cve-data-dir build:docs-ci --action_env=DOCS_RST_CHECK=1 --host_action_env=DOCS_RST_CHECK=1 @@ -437,6 +444,11 @@ common:engflow-common --credential_helper=*.engflow.com=%workspace%/bazel/engflo common:engflow-common --grpc_keepalive_time=60s common:engflow-common --grpc_keepalive_timeout=30s common:engflow-common --remote_cache_compression +# Recover from transient gRPC failures (e.g. UNAVAILABLE: io exception) talking to engflow. +common:engflow-common --remote_retries=10 +common:engflow-common --remote_retry_max_delay=60s +# Recover when blobs are evicted from the CAS mid-build (common during patch releases). +common:engflow-common --experimental_remote_cache_eviction_retries=5 # this provides access to RBE+cache common:rbe --config=remote-cache @@ -445,6 +457,7 @@ common:rbe --config=remote-exec # this provides access to just cache common:remote-cache --config=engflow-common common:remote-cache --remote_cache=grpcs://mordenite.cluster.engflow.com +common:remote-cache --experimental_remote_downloader=grpcs://mordenite.cluster.engflow.com common:remote-cache --remote_timeout=3600s common:remote-exec --remote_executor=grpcs://mordenite.cluster.engflow.com diff --git a/.bazelversion b/.bazelversion index 5942a0d3a0e74..df5119ec64e60 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.7.1 +8.7.0 diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index fe6e0aa79b2c1..3c743e9ca6948 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash -echo "build --config=clang" >> user.bazelrc +if [[ ! -f user.bazelrc ]] || ! grep -Fxq 'build --config=clang' user.bazelrc; then + echo -e "build --config=clang" >> user.bazelrc +fi # Ideally we want this line so bazel doesn't pollute things outside of the devcontainer, but some of # API tooling (proto_sync) depends on symlink like bazel-bin. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 53a28632fc9f3..2f8baf66b3a57 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,7 @@ blank_issues_enabled: false contact_links: - name: "Crash bug" - url: https://github.com/envoyproxy/envoy/security/policy - about: "Please file any crash bug (including asserts in debug builds) with envoy-security@googlegroups.com." + url: https://github.com/envoyproxy/envoy/security/advisories/new + about: | + Please report any crash bug (including asserts in debug builds) by opening a GitHub Security Advisory. + Alternatively, please email envoy-security@googlegroups.com." diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md index 98cc3b7808c98..8c7f11f564e94 100644 --- a/.github/ISSUE_TEMPLATE/other.md +++ b/.github/ISSUE_TEMPLATE/other.md @@ -8,8 +8,11 @@ assignees: '' --- **If you are reporting *any* crash or *any* potential security issue, *do not* -open an issue in this repo. Please report the issue via emailing -envoy-security@googlegroups.com where the issue will be triaged appropriately.** +open an issue in this repo. Instead, please +[open a GitHub Security Advisory](https://github.com/envoyproxy/envoy/security/advisories/new) +(preferred). + +Alternatively, you may email envoy-security@googlegroups.com.** *Title*: *One line description* diff --git a/.github/config.yml b/.github/config.yml index d474f951dc07b..0081b12df6afb 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -4,16 +4,16 @@ build-image: repo: docker.io/envoyproxy/envoy-build repo-gcr: gcr.io/envoy-ci/envoy-build # default ci caching (ci) - sha: 20656853fae51927cda557e7af80ccff175f5de6f84bd0f092cd8672b2a6e0fe - sha-ci: 20656853fae51927cda557e7af80ccff175f5de6f84bd0f092cd8672b2a6e0fe - sha-devtools: 6e7a82d4f1ba040f4ebef0c1aae00cdbd205ff7a1284c20cc20984fdfa4a91d8 - sha-docker: 85b6c3e76f093d9c9d10a968b5615cc8d82f38d7aef311100d542e4d640f5a74 - sha-gcc: 439e870260c1599646d05b8b5d3bf1b6dd585c2e3cdac78dcb9f4081564c27fd - sha-mobile: bd1338a8951376211e4f4f6ff3171675670c4c582b0966f1d247abd3ba6a8a67 - sha-worker: 25a68eff24b7414a346977d545687b87851d1c5746c466798050fa12fc5d0686 + sha: 0b285a2c5e8fd85db238a6159f9748dac38569f081501bd508876076fecc3fe5 + sha-ci: 0b285a2c5e8fd85db238a6159f9748dac38569f081501bd508876076fecc3fe5 + sha-devtools: 9e7dee3f9ce05274cfeb79a629b6b4f9163edb7bed436daa567ca7b58a35dfac + sha-docker: 511f15efe9e5fdb44f290e271bea982faaf279222ea36f9109c2ddc32d99931a + sha-gcc: 61d798d4385162ba52b7f80316a15bea096feaa255e61365f842e9902843d6c4 + sha-mobile: a80317cd73e52bea46caa2fd7d12142f2afdaca372a903523e42b00bc5266680 + sha-worker: 936a89f86a2dff47a2027031a7c330e97eb05339c93d6742b0a2d9adc6680a12 # TODO: remove this dupe (currently used by ci request handler) - mobile-sha: bd1338a8951376211e4f4f6ff3171675670c4c582b0966f1d247abd3ba6a8a67 - tag: 86873047235e9b8232df989a5999b9bebf9db69c + mobile-sha: a80317cd73e52bea46caa2fd7d12142f2afdaca372a903523e42b00bc5266680 + tag: v0.1.6 config: envoy: @@ -89,11 +89,6 @@ checks: required: true on-run: - mobile-ios-tests - mobile-perf: - name: Mobile/Perf - required: true - on-run: - - mobile-perf mobile-python: name: Mobile/Python required: true diff --git a/.github/workflows/_check_build.yml b/.github/workflows/_check_build.yml index ab1076cd161de..4a4492cbe96bf 100644 --- a/.github/workflows/_check_build.yml +++ b/.github/workflows/_check_build.yml @@ -5,6 +5,9 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: + slack-bot-token: inputs: request: type: string @@ -20,6 +23,9 @@ concurrency: jobs: build: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} + slack-bot-token: ${{ secrets.slack-bot-token }} permissions: actions: read contents: read @@ -38,7 +44,9 @@ jobs: Error: rbe: true request: ${{ inputs.request }} + catch-errors: ${{ matrix.catch-errors || false }} skip: ${{ matrix.skip != false && true || false }} + slack-channel: ${{ matrix.slack-channel || '' }} target: ${{ matrix.target }} timeout-minutes: 180 trusted: ${{ inputs.trusted }} @@ -56,3 +64,5 @@ jobs: - target: openssl name: OpenSSL skip: ${{ ! fromJSON(inputs.request).run.check-build-openssl }} + catch-errors: true + slack-channel: '#envoy-openssl' diff --git a/.github/workflows/_check_coverage.yml b/.github/workflows/_check_coverage.yml index 310395dfc9ee3..517f0ca224114 100644 --- a/.github/workflows/_check_coverage.yml +++ b/.github/workflows/_check_coverage.yml @@ -6,6 +6,7 @@ permissions: on: workflow_call: secrets: + dockerhub-token: gcp-key: required: true @@ -24,6 +25,8 @@ concurrency: jobs: coverage: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -58,7 +61,7 @@ jobs: upload-name: coverage upload-path: generated/coverage/html steps-post: | - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: output-path: generated/coverage/html/gcs-metadata.json input-format: yaml @@ -79,7 +82,7 @@ jobs: - target: fuzz_coverage name: Fuzz coverage steps-post: | - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: output-path: generated/fuzz_coverage/html/gcs-metadata.json input-format: yaml diff --git a/.github/workflows/_check_runtime.yml b/.github/workflows/_check_runtime.yml index 9048b7bb03088..99a70c4db920d 100644 --- a/.github/workflows/_check_runtime.yml +++ b/.github/workflows/_check_runtime.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: request: type: string @@ -20,6 +22,8 @@ concurrency: jobs: runtime: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read diff --git a/.github/workflows/_check_san.yml b/.github/workflows/_check_san.yml index a306167844317..132e31a6a745c 100644 --- a/.github/workflows/_check_san.yml +++ b/.github/workflows/_check_san.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: request: type: string @@ -20,6 +22,8 @@ concurrency: jobs: san: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read diff --git a/.github/workflows/_cve_fetch.yml b/.github/workflows/_cve_fetch.yml index c8e63432f1752..d6432cccf51f7 100644 --- a/.github/workflows/_cve_fetch.yml +++ b/.github/workflows/_cve_fetch.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set vars id: vars run: | @@ -35,7 +35,7 @@ jobs: else echo "weekly_run=false" >> $GITHUB_OUTPUT fi - - uses: envoyproxy/toolshed/actions/gcp/setup@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/gcp/setup@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Setup GCP with: key: ${{ secrets.gcs-cve-key }} diff --git a/.github/workflows/_cve_scan.yml b/.github/workflows/_cve_scan.yml index 58237c3d511a0..51077fd8f404a 100644 --- a/.github/workflows/_cve_scan.yml +++ b/.github/workflows/_cve_scan.yml @@ -23,12 +23,12 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set vars id: vars run: | echo "cve-data-path=${{ inputs.cve-data-path }}" > $GITHUB_OUTPUT - - uses: envoyproxy/toolshed/actions/gcp/setup@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/gcp/setup@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Setup GCP with: key: ${{ secrets.gcs-cve-key }} diff --git a/.github/workflows/_finish.yml b/.github/workflows/_finish.yml index 99d3b5da5e7f8..d54566adb340d 100644 --- a/.github/workflows/_finish.yml +++ b/.github/workflows/_finish.yml @@ -36,7 +36,7 @@ jobs: actions: read contents: read steps: - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Incoming data id: needs with: @@ -87,7 +87,7 @@ jobs: summary: "Check has finished", text: $text}}}} - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Print summary with: input: ${{ toJSON(steps.needs.outputs.value).summary-title }} @@ -95,13 +95,13 @@ jobs: "## \(.)" options: -Rr output-path: GITHUB_STEP_SUMMARY - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/actions/github/checks@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checks@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Update check with: action: update diff --git a/.github/workflows/_load.yml b/.github/workflows/_load.yml index dc8c9a811832c..3ab02410c91c0 100644 --- a/.github/workflows/_load.yml +++ b/.github/workflows/_load.yml @@ -87,20 +87,22 @@ jobs: gh api \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ - /repos/${{ github.repository }}/actions/runs/${{ inputs.run-id }} \ + "/repos/${GH_REPO}/actions/runs/${RUN_ID}" \ | jq '.' - RUNID=$(gh run view ${{ inputs.run-id }} --repo ${{ github.repository }} --json databaseId | jq -r '.databaseId') + RUNID=$(gh run view "${RUN_ID}" --repo "${GH_REPO}" --json databaseId | jq -r '.databaseId') echo "value=${RUNID}" >> "$GITHUB_OUTPUT" id: run-id if: ${{ inputs.runs-after == true }} env: GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ inputs.run-id }} + GH_REPO: ${{ github.repository }} # Load env data # Handle any failure in triggering job # Remove any `checks` we dont care about # Prepare a check request - - uses: envoyproxy/toolshed/actions/github/env/load@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/env/load@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Load env id: data with: @@ -111,13 +113,13 @@ jobs: GH_TOKEN: ${{ github.token }} # Update the check - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/actions/github/checks@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checks@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Update check if: ${{ fromJSON(steps.data.outputs.data).data.check.action == 'RUN' }} with: @@ -125,7 +127,7 @@ jobs: checks: ${{ toJSON(fromJSON(steps.data.outputs.data).checks) }} token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Print request summary with: input: | @@ -145,7 +147,7 @@ jobs: | $summary.summary as $summary | "${{ inputs.template-request-summary }}" - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: request-output name: Load request with: diff --git a/.github/workflows/_load_env.yml b/.github/workflows/_load_env.yml index 426fda17765b9..4fdc144736595 100644 --- a/.github/workflows/_load_env.yml +++ b/.github/workflows/_load_env.yml @@ -55,7 +55,6 @@ env: jobs: request: - if: ${{ github.repository == 'envoyproxy/envoy' || vars.ENVOY_CI }} runs-on: ubuntu-24.04 outputs: build-image: ${{ toJSON(fromJSON(steps.env.outputs.data).request.build-image) }} @@ -63,18 +62,18 @@ jobs: request: ${{ steps.env.outputs.data }} trusted: true steps: - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: started name: Create timestamp with: options: -r filter: | now - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout name: Checkout Envoy repository - name: Generate environment variables - uses: envoyproxy/toolshed/actions/envoy/ci/env@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/envoy/ci/env@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: env with: branch-name: ${{ inputs.branch-name }} @@ -86,7 +85,7 @@ jobs: - name: Request summary id: summary - uses: envoyproxy/toolshed/actions/github/env/summary@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/env/summary@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: actor: ${{ toJSON(fromJSON(steps.env.outputs.data).request.actor) }} base-sha: ${{ fromJSON(steps.env.outputs.data).request.base-sha }} @@ -110,5 +109,5 @@ jobs: needs: request if: ${{ inputs.cache-docker }} with: - request: ${{ toJSON(needs.request.outputs) }} + caches: ${{ needs.request.outputs.caches }} image-tag: ${{ fromJSON(needs.request.outputs.build-image).default }} diff --git a/.github/workflows/_mobile_container_ci.yml b/.github/workflows/_mobile_container_ci.yml index bd7d1eb7024c5..2977313af3859 100644 --- a/.github/workflows/_mobile_container_ci.yml +++ b/.github/workflows/_mobile_container_ci.yml @@ -8,6 +8,7 @@ on: secrets: app-id: app-key: + dockerhub-token: rbe-key: ssh-key-extra: inputs: @@ -115,6 +116,7 @@ on: type: string timeout-minutes: type: number + default: 60 trusted: type: boolean default: false @@ -132,14 +134,15 @@ on: jobs: ci: - uses: ./.github/workflows/_run.yml - name: ${{ inputs.target }} + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} + ssh-key-extra: ${{ secrets.ssh-key-extra }} permissions: actions: read contents: read packages: read - secrets: - ssh-key-extra: ${{ secrets.ssh-key-extra }} + uses: ./.github/workflows/_run.yml + name: ${{ inputs.target }} with: args: ${{ inputs.args }} rbe: ${{ inputs.rbe }} diff --git a/.github/workflows/_precheck_deps.yml b/.github/workflows/_precheck_deps.yml index 6c59b976c4c6a..62a0ca7ec5c09 100644 --- a/.github/workflows/_precheck_deps.yml +++ b/.github/workflows/_precheck_deps.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: dependency-review: type: boolean @@ -23,6 +25,8 @@ concurrency: jobs: deps: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -52,7 +56,7 @@ jobs: if: ${{ inputs.dependency-review }} steps: - name: Checkout Repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ fromJSON(inputs.request).request.sha }} persist-credentials: false diff --git a/.github/workflows/_precheck_external.yml b/.github/workflows/_precheck_external.yml index 00144f9dd26f8..915157206b3af 100644 --- a/.github/workflows/_precheck_external.yml +++ b/.github/workflows/_precheck_external.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: request: type: string @@ -20,6 +22,8 @@ concurrency: jobs: external: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read diff --git a/.github/workflows/_precheck_format.yml b/.github/workflows/_precheck_format.yml index ae63eb19277fc..5a9ec6a4fa2f9 100644 --- a/.github/workflows/_precheck_format.yml +++ b/.github/workflows/_precheck_format.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: request: type: string @@ -21,6 +23,8 @@ concurrency: jobs: format: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read diff --git a/.github/workflows/_precheck_publish.yml b/.github/workflows/_precheck_publish.yml index b750b5bc493a9..48cbc6fe720f3 100644 --- a/.github/workflows/_precheck_publish.yml +++ b/.github/workflows/_precheck_publish.yml @@ -6,6 +6,7 @@ permissions: on: workflow_call: secrets: + dockerhub-token: gcp-key: required: true inputs: @@ -23,6 +24,8 @@ concurrency: jobs: publish: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -85,7 +88,7 @@ jobs: upload-name: docs upload-path: generated/docs steps-post: | - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: output-path: generated/docs/gcs-metadata.json input-format: yaml diff --git a/.github/workflows/_publish_build.yml b/.github/workflows/_publish_build.yml index 9c21b91cd31fb..8976aa87d6583 100644 --- a/.github/workflows/_publish_build.yml +++ b/.github/workflows/_publish_build.yml @@ -6,6 +6,7 @@ permissions: on: workflow_call: secrets: + dockerhub-token: gpg-key: required: true gpg-key-password: @@ -33,6 +34,8 @@ concurrency: jobs: binary: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -58,6 +61,8 @@ jobs: upload-path: container/envoy/${{ inputs.arch }}/bin/ docker: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -87,13 +92,14 @@ jobs: runs-on: ${{ inputs.arch == 'arm64' && (vars.ENVOY_ARM_VM || 'ubuntu-24.04-arm') || null }} distribution: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} + gpg-key: ${{ secrets.gpg-key }} + gpg-key-password: ${{ secrets.gpg-key-password }} permissions: actions: read contents: read packages: read - secrets: - gpg-key: ${{ secrets.gpg-key }} - gpg-key-password: ${{ secrets.gpg-key-password }} name: Packages needs: - binary diff --git a/.github/workflows/_publish_release.yml b/.github/workflows/_publish_release.yml index 6a82a20ed3e0d..dcf3f2e347809 100644 --- a/.github/workflows/_publish_release.yml +++ b/.github/workflows/_publish_release.yml @@ -6,8 +6,8 @@ permissions: on: workflow_call: secrets: - dockerhub-password: - dockerhub-username: + dockerhub-token: + dockerhub-write-token: ENVOY_CI_SYNC_APP_ID: ENVOY_CI_SYNC_APP_KEY: ENVOY_CI_PUBLISH_APP_ID: @@ -17,6 +17,9 @@ on: gpg-key-password: required: true inputs: + dockerhub-username: + type: string + required: true request: type: string required: true @@ -35,19 +38,22 @@ concurrency: jobs: sign: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} + gpg-key: ${{ secrets.gpg-key }} + gpg-key-password: ${{ secrets.gpg-key-password }} permissions: actions: read contents: read packages: read - secrets: - gpg-key: ${{ secrets.gpg-key }} - gpg-key-password: ${{ secrets.gpg-key-password }} if: ${{ vars.ENVOY_CI_RELEASE || github.repository == 'envoyproxy/envoy' }} name: Sign packages uses: ./.github/workflows/_run.yml with: target: release.signed bazel-extra: >- + --config=rbe + --noremote_upload_local_results --//distribution:x64-packages=//distribution:custom/x64/packages.x64.tar.gz --//distribution:arm64-packages=//distribution:custom/arm64/packages.arm64.tar.gz --//distribution:x64-release=//distribution:custom/x64/bin/release.tar.zst @@ -73,8 +79,7 @@ jobs: container: secrets: - dockerhub-username: ${{ secrets.dockerhub-username }} - dockerhub-password: ${{ secrets.dockerhub-password }} + dockerhub-write-token: ${{ secrets.dockerhub-write-token }} permissions: actions: read contents: read @@ -83,6 +88,7 @@ jobs: uses: ./.github/workflows/_publish_release_container.yml with: dockerhub-repo: ${{ vars.DOCKERHUB_REPO || 'envoy' }} + dockerhub-username: ${{ inputs.dockerhub-username }} dev: ${{ fromJSON(inputs.request).request.version.dev }} sha: ${{ fromJSON(inputs.request).request.sha }} target-branch: ${{ fromJSON(inputs.request).request.target-branch }} @@ -95,6 +101,7 @@ jobs: secrets: app-id: ${{ inputs.trusted && secrets.ENVOY_CI_PUBLISH_APP_ID || '' }} app-key: ${{ inputs.trusted && secrets.ENVOY_CI_PUBLISH_APP_KEY || '' }} + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -121,10 +128,6 @@ jobs: include: - target: publish name: github - source: | - export ENVOY_COMMIT=${{ fromJSON(inputs.request).request.sha }} - export ENVOY_REPO=${{ github.repository }} - export ENVOY_PUBLISH_DRY_RUN=${{ (fromJSON(inputs.request).request.version.dev || ! inputs.trusted) && 1 || '' }} docs: # For normal commits to Envoy main this will trigger an update in the website repo, @@ -139,12 +142,12 @@ jobs: needs: - release steps: - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: appauth with: app_id: ${{ secrets.ENVOY_CI_SYNC_APP_ID }} key: ${{ secrets.ENVOY_CI_SYNC_APP_KEY }} - - uses: envoyproxy/toolshed/actions/dispatch@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/dispatch@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: ref: main repository: ${{ fromJSON(inputs.request).request.version.dev && 'envoyproxy/envoy-website' || 'envoyproxy/archive' }} diff --git a/.github/workflows/_publish_release_container.yml b/.github/workflows/_publish_release_container.yml index 1c35d097fc9bd..5a23c59651593 100644 --- a/.github/workflows/_publish_release_container.yml +++ b/.github/workflows/_publish_release_container.yml @@ -6,8 +6,7 @@ permissions: on: workflow_call: secrets: - dockerhub-password: - dockerhub-username: + dockerhub-write-token: inputs: dev: required: true @@ -17,6 +16,9 @@ on: required: true default: envoy type: string + dockerhub-username: + required: true + type: string sha: required: true type: string @@ -56,7 +58,7 @@ jobs: - name: Generate manifest configuration (dev) id: dev-config if: ${{ inputs.dev && inputs.target-branch == 'main' }} - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: input-format: yaml filter: >- @@ -135,7 +137,7 @@ jobs: - tools-dev-${{ github.sha }} - name: Generate manifest configuration (release) - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: release-config if: ${{ ! inputs.dev || ! inputs.target-branch != 'main' }} with: @@ -225,10 +227,10 @@ jobs: - tools-v${{ inputs.version-major }}.${{ inputs.version-minor }}-latest - name: Collect and push OCI artifacts - uses: envoyproxy/toolshed/actions/oci/collector@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/oci/collector@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: artifacts-pattern: oci.* manifest-config: ${{ steps.dev-config.outputs.value || steps.release-config.outputs.value }} dry-run: ${{ ! inputs.trusted || (inputs.target-branch != 'main' && inputs.dev) }} - dockerhub-username: ${{ inputs.trusted && secrets.dockerhub-username || '' }} - dockerhub-password: ${{ inputs.trusted && secrets.dockerhub-password || '' }} + dockerhub-username: ${{ inputs.trusted && inputs.dockerhub-username || '' }} + dockerhub-password: ${{ inputs.trusted && secrets.dockerhub-write-token || '' }} diff --git a/.github/workflows/_publish_verify.yml b/.github/workflows/_publish_verify.yml index 283e1e1d60435..0700c4e4eae54 100644 --- a/.github/workflows/_publish_verify.yml +++ b/.github/workflows/_publish_verify.yml @@ -5,6 +5,8 @@ permissions: on: workflow_call: + secrets: + dockerhub-token: inputs: request: type: string @@ -24,6 +26,8 @@ concurrency: jobs: examples: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -76,6 +80,8 @@ jobs: shell: bash distroless: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read @@ -108,7 +114,7 @@ jobs: export NO_BUILD_SETUP=1 steps-pre: | - id: version-support - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: input: | version_major: ${{ fromJSON(inputs.request).request.version.major }} @@ -134,6 +140,8 @@ jobs: shell: bash distro: + secrets: + dockerhub-token: ${{ secrets.dockerhub-token }} permissions: actions: read contents: read diff --git a/.github/workflows/_request.yml b/.github/workflows/_request.yml index f3f2e998bc543..cdc4e08408d10 100644 --- a/.github/workflows/_request.yml +++ b/.github/workflows/_request.yml @@ -55,14 +55,14 @@ jobs: caches: ${{ steps.caches.outputs.value }} config: ${{ steps.config.outputs.config }} steps: - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: started name: Create timestamp with: options: -r filter: | now - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout name: Checkout Envoy repository (requested) with: @@ -76,7 +76,7 @@ jobs: # *ALL* variables collected should be treated as untrusted and should be sanitized before # use - name: Generate environment variables from commit - uses: envoyproxy/toolshed/actions/envoy/ci/request@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/envoy/ci/request@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: env with: branch-name: ${{ steps.checkout.outputs.branch-name }} @@ -87,7 +87,7 @@ jobs: vars: ${{ toJSON(vars) }} working-directory: requested - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout-target name: Checkout Envoy repository (target branch) with: @@ -95,7 +95,7 @@ jobs: config: | fetch-depth: 1 path: target - - uses: envoyproxy/toolshed/actions/hashfiles@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/hashfiles@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: bazel-cache-hash name: Bazel cache hash with: @@ -104,7 +104,7 @@ jobs: - name: Request summary id: summary - uses: envoyproxy/toolshed/actions/github/env/summary@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/env/summary@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: actor: ${{ toJSON(fromJSON(steps.env.outputs.data).request.actor) }} base-sha: ${{ fromJSON(steps.env.outputs.data).request.base-sha }} @@ -120,28 +120,28 @@ jobs: target-branch: ${{ fromJSON(steps.env.outputs.data).request.target-branch }} - id: cache-id-bazel-x64 - uses: envoyproxy/toolshed/actions/github/artifact/cache/id@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/artifact/cache/id@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: name: ${{ steps.bazel-cache-hash.outputs.value }}-x64 wf-path: .github/workflows/request.yml - id: cache-id-bazel-arm64 - uses: envoyproxy/toolshed/actions/github/artifact/cache/id@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/artifact/cache/id@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: name: ${{ steps.bazel-cache-hash.outputs.value }}-arm64 wf-path: .github/workflows/request.yml - id: cache-id-bazel-docs-x64 - uses: envoyproxy/toolshed/actions/github/artifact/cache/id@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/artifact/cache/id@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: name: ${{ steps.bazel-cache-hash.outputs.value }}-docs-x64 wf-path: .github/workflows/request.yml - id: cache-id-bazel-external-x64 - uses: envoyproxy/toolshed/actions/github/artifact/cache/id@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/artifact/cache/id@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: name: ${{ steps.bazel-cache-hash.outputs.value }}-external-x64 wf-path: .github/workflows/request.yml - name: Environment data - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: data with: input: | @@ -176,20 +176,20 @@ jobs: # TODO(phlax): shift this to ci/request action above - name: Check Docker cache (x64) id: cache-exists-docker-x64 - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: lookup-only: true path: /tmp/cache key: ${{ fromJSON(steps.data.outputs.value).request.build-image.default }} - name: Check Docker cache (arm64) id: cache-exists-docker-arm64 - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: lookup-only: true path: /tmp/cache key: ${{ fromJSON(steps.data.outputs.value).request.build-image.default }}-arm64 - name: Caches - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: caches with: input-format: yaml @@ -205,8 +205,8 @@ jobs: target-branch: ${{ fromJSON(steps.env.outputs.data).request.target-branch }} filter: | .["target-branch"] as $branch - | if ($branch | test("^release/v[0-9]+\\.[0-9]+$")) then - ($branch | sub("^release/v"; "") + ".0") as $version_str + | if ($branch | test("^(release/v|patches/)[0-9]+\\.[0-9]+$")) then + ($branch | sub("^(release/v|patches/)"; "") + ".0") as $version_str | ($version_str | utils::version) as $version | if ($version.major < 1 or ($version.major == 1 and $version.minor <= 37)) then .bazel["docs-x64"] = "skip" diff --git a/.github/workflows/_request_cache_bazel.yml b/.github/workflows/_request_cache_bazel.yml index 47cdf21971f36..56508565a7030 100644 --- a/.github/workflows/_request_cache_bazel.yml +++ b/.github/workflows/_request_cache_bazel.yml @@ -59,7 +59,7 @@ jobs: && ! fromJSON(inputs.caches).bazel[format('{0}-{1}', inputs.output-base, inputs.arch)]) }} steps: - - uses: envoyproxy/toolshed/actions/bind-mounts@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bind-mounts@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: mounts: | - src: /mnt/workspace @@ -73,9 +73,9 @@ jobs: target: /build chown: "runner:docker" - name: Free diskspace - uses: envoyproxy/toolshed/actions/diskspace@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/diskspace@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: inputs.arch == 'x64' && github.event.repository.private - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout-target name: Checkout Envoy repository (target branch) with: @@ -83,14 +83,14 @@ jobs: config: | fetch-depth: 1 - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: appauth name: Appauth (mutex lock) with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/actions/cache/prime@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/cache/prime@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: bazel-cache name: Prime Bazel cache with: @@ -109,9 +109,9 @@ jobs: cd /source export BAZEL_BUILD_EXTRA_OPTIONS="--config=ci --config=rbe" export ENVOY_CACHE_ROOT=/build/bazel_root - export ENVOY_CACHE_OUTPUT_BASE="${{ inputs.output-base }}" - export ENVOY_CACHE_TARGETS=$(echo "${{ inputs.targets }}" | sed 's/ / + /g') - export ENVOY_CACHE_WORKING_DIR="${{ inputs.working-dir }}" + export ENVOY_CACHE_OUTPUT_BASE="${INPUT_OUTPUT_BASE}" + export ENVOY_CACHE_TARGETS=$(echo "${INPUT_TARGETS}" | sed 's/ / + /g') + export ENVOY_CACHE_WORKING_DIR="${INPUT_WORKING_DIR}" # ironically the repository_cache is just about the only thing you dont want to cache export ENVOY_REPOSITORY_CACHE=/tmp/cache ./ci/do_ci.sh cache-create @@ -128,3 +128,6 @@ jobs: run-as-sudo: false env: GITHUB_TOKEN: ${{ github.token }} + INPUT_OUTPUT_BASE: ${{ inputs.output-base }} + INPUT_TARGETS: ${{ inputs.targets }} + INPUT_WORKING_DIR: ${{ inputs.working-dir }} diff --git a/.github/workflows/_request_cache_docker.yml b/.github/workflows/_request_cache_docker.yml index 751dacba5d6fe..01da07ef8d34b 100644 --- a/.github/workflows/_request_cache_docker.yml +++ b/.github/workflows/_request_cache_docker.yml @@ -39,7 +39,7 @@ on: # For a job that does, you can restore with something like: # # steps: -# - uses: envoyproxy/toolshed/actions/docker/cache/restore@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 +# - uses: envoyproxy/toolshed/actions/docker/cache/restore@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 # with: # key: "${{ needs.env.outputs.build-image }}" # @@ -51,13 +51,13 @@ jobs: name: "[${{ inputs.arch }}] Prime Docker cache" if: ${{ ! fromJSON(inputs.caches).docker[inputs.arch] }} steps: - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: appauth name: Appauth (mutex lock) with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/actions/docker/cache/prime@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/docker/cache/prime@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: docker name: Prime Docker cache (${{ inputs.image-tag }}${{ inputs.cache-suffix }}) with: @@ -65,7 +65,7 @@ jobs: key-suffix: ${{ inputs.cache-suffix }} lock-token: ${{ steps.appauth.outputs.token }} lock-repository: ${{ inputs.lock-repository }} - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: data name: Cache data with: @@ -73,7 +73,7 @@ jobs: input: | cached: ${{ steps.docker.outputs.cached }} key: ${{ inputs.image-tag }}${{ inputs.cache-suffix }} - - uses: envoyproxy/toolshed/actions/json/table@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/json/table@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Summary with: json: ${{ steps.data.outputs.value }} diff --git a/.github/workflows/_request_checks.yml b/.github/workflows/_request_checks.yml index c663eeb4b3ead..465111305067a 100644 --- a/.github/workflows/_request_checks.yml +++ b/.github/workflows/_request_checks.yml @@ -55,7 +55,7 @@ jobs: runs-on: ${{ fromJSON(inputs.env).config.ci.agent-ubuntu }} name: Start checks steps: - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: check-config name: Prepare check data with: @@ -78,13 +78,13 @@ jobs: | .skipped.output.summary = "${{ inputs.skipped-summary }}" | .skipped.output.text = "" - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Appauth id: appauth with: app_id: ${{ secrets.app-id }} key: ${{ secrets.app-key }} - - uses: envoyproxy/toolshed/actions/github/checks@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checks@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Start checks id: checks with: @@ -95,7 +95,7 @@ jobs: ${{ fromJSON(inputs.env).summary.summary }} token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/actions/json/table@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/json/table@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Summary with: collapse-open: true @@ -119,7 +119,7 @@ jobs: output-path: GITHUB_STEP_SUMMARY title: Checks started/skipped - - uses: envoyproxy/toolshed/actions/github/env/save@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/env/save@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Save env id: data with: diff --git a/.github/workflows/_run.yml b/.github/workflows/_run.yml index 289ba04a59af0..04af39c9e704c 100644 --- a/.github/workflows/_run.yml +++ b/.github/workflows/_run.yml @@ -8,8 +8,10 @@ on: secrets: app-id: app-key: + dockerhub-token: gpg-key: gpg-key-password: + slack-bot-token: ssh-key: ssh-key-extra: inputs: @@ -81,9 +83,6 @@ on: docker-ipv6: default: true type: boolean - dockerhub-username: - default: envoyproxy - type: string downloads: type: string entrypoint: @@ -138,12 +137,16 @@ on: skip: type: boolean default: false + slack-channel: + type: string + default: '' + description: Slack channel for failure notifications (opt-in, e.g. '#envoy-ci'). Leave empty to disable. source: type: string summary-post: type: string default: | - - uses: envoyproxy/toolshed/actions/envoy/run/summary@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/run/summary@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: context: %{{ inputs.context }} steps-pre: @@ -212,7 +215,7 @@ jobs: name: ${{ inputs.target-suffix && format('[{0}] ', inputs.target-suffix) || '' }}${{ inputs.command }} ${{ inputs.target }} timeout-minutes: ${{ inputs.timeout-minutes }} steps: - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: started name: Create timestamp with: @@ -220,7 +223,7 @@ jobs: filter: | now # This controls which input vars are exposed to the run action (and related steps) - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Context id: context with: @@ -241,7 +244,7 @@ jobs: | . * {$config, $check} - run: | - mkdir ${{ runner.temp }}/container + mkdir "${RUNNER_TEMP}/container" MNT_AVAILABLE=false if mountpoint -q /mnt; then MNT_AVAILABLE=true @@ -253,14 +256,14 @@ jobs: fi echo "mnt-available=$MNT_AVAILABLE" >> "$GITHUB_OUTPUT" id: disk - - uses: envoyproxy/toolshed/actions/github/remnt@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/remnt@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: steps.disk.outputs.should-remnt == 'true' - - uses: envoyproxy/toolshed/actions/bind-mounts@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bind-mounts@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: inputs.bind-mount && steps.disk.outputs.mnt-available == 'true' with: mounts: ${{ inputs.bind-mounts }} - name: Free diskspace - uses: envoyproxy/toolshed/actions/diskspace@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/diskspace@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: inputs.diskspace-hack || steps.disk.outputs.mnt-available != 'true' with: to_remove: ${{ inputs.diskspace-hack-paths }} @@ -268,7 +271,7 @@ jobs: mount df -h - - uses: envoyproxy/toolshed/actions/bson@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bson@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Configure Docker if: runner.os == 'Linux' with: @@ -284,7 +287,7 @@ jobs: | "${{ inputs.template-docker-configure }}" # Caches - - uses: envoyproxy/toolshed/actions/cache/restore@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/cache/restore@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: >- fromJSON(inputs.bazel-cache) name: >- @@ -318,19 +321,19 @@ jobs: # HACK/WORKAROUND for cache scope issue (https://github.com/envoyproxy/envoy/issues/37603) - if: ${{ inputs.cache-build-image }} id: cache-lookup - uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + uses: actions/cache/restore@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0 with: lookup-only: true path: /tmp/cache key: ${{ inputs.cache-build-image }}${{ inputs.cache-build-image-key-suffix }} - if: ${{ inputs.cache-build-image && steps.cache-lookup.outputs.cache-hit == 'true' }} name: Restore Docker cache ${{ inputs.cache-build-image && format('({0})', inputs.cache-build-image) || '' }} - uses: envoyproxy/toolshed/actions/docker/cache/restore@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/docker/cache/restore@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: image-tag: ${{ inputs.cache-build-image }} key-suffix: ${{ inputs.cache-build-image-key-suffix }} - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: appauth name: Appauth if: ${{ inputs.trusted }} @@ -341,7 +344,7 @@ jobs: # - the workaround is to allow the token to be passed through. token: ${{ github.token }} token-ok: true - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout name: Checkout Envoy repository with: @@ -357,7 +360,7 @@ jobs: token: ${{ inputs.trusted && steps.appauth.outputs.token || github.token }} # This is currently only use by mobile-docs and can be removed once they are updated to the newer website - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout-extra name: Checkout extra repository (for publishing) if: ${{ inputs.checkout-extra }} @@ -366,7 +369,7 @@ jobs: ssh-key: ${{ inputs.trusted && inputs.ssh-key-extra || '' }} - name: Import GPG key - uses: envoyproxy/toolshed/actions/gpg/import@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/gpg/import@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: ${{ inputs.import-gpg }} with: key: ${{ secrets.gpg-key }} @@ -380,13 +383,32 @@ jobs: if: >- ${{ fromJSON(inputs.request).request.pr != '' }} - run: | - echo "${{ vars.ENVOY_CI_BAZELRC }}" > repo.bazelrc + echo "${BAZELRC_CONTENT}" > repo.bazelrc if: ${{ vars.ENVOY_CI_BAZELRC }} name: Configure repo Bazel settings + env: + BAZELRC_CONTENT: ${{ vars.ENVOY_CI_BAZELRC }} + + - id: dockerhub + if: inputs.container-command != '' + run: | + if [[ -n "$DOCKERHUB_TOKEN" ]]; then + echo "login=true" >> "$GITHUB_OUTPUT" + fi + env: + DOCKERHUB_TOKEN: ${{ secrets.dockerhub-token }} + - name: Login to Docker Hub (read-only) + if: inputs.container-command != '' && steps.dockerhub.outputs.login == 'true' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.dockerhub-token }} + continue-on-error: true # NOTE: This is where untrusted code can be run!!! - # It MUST be the last step in the workflow - - uses: envoyproxy/toolshed/actions/github/run@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + # Only post-failure notification steps (which don't use any repo code) should follow this step. + - uses: envoyproxy/toolshed/actions/github/run@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 + id: ci-run name: Run CI ${{ inputs.command }} ${{ inputs.target }} with: args: ${{ inputs.args != '--' && inputs.args || inputs.target }} @@ -438,3 +460,27 @@ jobs: MOUNT_GPG_HOME: ${{ inputs.import-gpg && 1 || '' }} ENVOY_DOCKER_CPUS: ${{ inputs.docker-cpus }} ENVOY_DOCKER_CI: ${{ inputs.docker-ci && 'true' || '' }} + ENVOY_COMMIT: ${{ fromJSON(inputs.request).request.sha }} + ENVOY_REPO: ${{ github.repository }} + ENVOY_PUBLISH_DRY_RUN: ${{ (fromJSON(inputs.request).request.version.dev || ! inputs.trusted) && 1 || '' }} + + - name: Notify Slack on failure + if: >- + steps.ci-run.outputs.exit-code != 0 + && inputs.slack-channel != '' + && github.event.workflow_run.event == 'push' + && github.repository == 'envoyproxy/envoy' + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + env: + JOB_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + method: chat.postMessage + token: ${{ secrets.slack-bot-token }} + payload: | + channel: "${{ inputs.slack-channel }}" + text: "CI job failed: ${{ inputs.target }}" + blocks: + - type: section + text: + type: mrkdwn + text: ":x: *CI job failed:* `${{ inputs.target }}`\n<${{ env.JOB_URL }}|View workflow run>" diff --git a/.github/workflows/_upload_gcs.yml b/.github/workflows/_upload_gcs.yml index 9662dd8dd9566..f17484eef0ce5 100644 --- a/.github/workflows/_upload_gcs.yml +++ b/.github/workflows/_upload_gcs.yml @@ -25,21 +25,21 @@ jobs: matrix: artifact: ${{ fromJSON(inputs.artifacts) }} steps: - - uses: actions/download-artifact@v8.0.1 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ matrix.artifact }} path: ${{ matrix.artifact }} - - uses: envoyproxy/toolshed/actions/jq@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/jq@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: metadata with: input: ${{ matrix.artifact }}/gcs-metadata.json input-format: json-path - run: | rm -rf ${{ matrix.artifact }}/gcs-metadata.json - - uses: envoyproxy/toolshed/actions/gcp/setup@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/gcp/setup@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: key: ${{ secrets.gcp-key }} - - uses: envoyproxy/toolshed/actions/gcs/artefact/sync@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/gcs/artefact/sync@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: bucket: ${{ fromJSON(steps.metadata.outputs.value).bucket }} path: ${{ matrix.artifact }} diff --git a/.github/workflows/codeql-daily.yml b/.github/workflows/codeql-daily.yml index a802944b3ba6b..0e5d71b3842c5 100644 --- a/.github/workflows/codeql-daily.yml +++ b/.github/workflows/codeql-daily.yml @@ -5,7 +5,8 @@ permissions: on: schedule: - - cron: '0 12 * * 4' + - cron: '0 12 * * *' + workflow_dispatch: concurrency: group: ${{ github.head_ref || github.run_id }}-${{ github.workflow }} @@ -27,7 +28,7 @@ jobs: steps: - - uses: envoyproxy/toolshed/actions/bind-mounts@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bind-mounts@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: | ! github.event.repository.private with: @@ -42,17 +43,17 @@ jobs: if: | env.BUILD_TARGETS != '' && github.event.repository.private - uses: envoyproxy/toolshed/actions/diskspace@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/diskspace@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: to_remove: | /usr/local/.ghcup /usr/local/lib/android - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # codeql-bundle-v4.35.1 + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # codeql-bundle-v4.36.1 with: languages: cpp trap-caching: false @@ -67,8 +68,15 @@ jobs: # - bazel/repository_locations.bzl # - .github/workflows/codeql-push.yml # - https://github.com/envoyproxy/envoy-build-tools/blob/main/build_container/build_container_ubuntu.sh#L84 - mkdir -p bin/clang18.1.8 - cd bin/clang18.1.8 + # + # IMPORTANT: the clang bundle MUST be unpacked outside $GITHUB_WORKSPACE (we use $RUNNER_TEMP). + # The CodeQL C++ extractor treats anything under the source root as project code, so headers + # under a checkout subdirectory would be analysed as Envoy source and produce false positives + # (e.g. cpp/new-free-mismatch in , alert #1881). Keeping it under $RUNNER_TEMP makes + # CodeQL classify those headers as system headers — same as /usr/include/c++/** on a normal + # build. Do NOT move this back into the checkout tree. + mkdir -p "${RUNNER_TEMP}/clang18.1.8" + cd "${RUNNER_TEMP}/clang18.1.8" wget -q https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/clang+llvm-18.1.8-x86_64-linux-gnu-ubuntu-18.04.tar.xz tar -xf clang+llvm-18.1.8-x86_64-linux-gnu-ubuntu-18.04.tar.xz --strip-components 1 @@ -77,7 +85,7 @@ jobs: bazelisk shutdown bazel build \ -c fastbuild \ - --repo_env=BAZEL_LLVM_PATH="$(realpath bin/clang18.1.8)" \ + --repo_env=BAZEL_LLVM_PATH="${RUNNER_TEMP}/clang18.1.8" \ --spawn_strategy=local \ --discard_analysis_cache \ --nouse_action_cache \ @@ -91,4 +99,4 @@ jobs: git clean -xdf - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # codeql-bundle-v4.35.1 + uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # codeql-bundle-v4.36.1 diff --git a/.github/workflows/codeql-push.yml b/.github/workflows/codeql-push.yml index 396806a486505..66cda63cdac68 100644 --- a/.github/workflows/codeql-push.yml +++ b/.github/workflows/codeql-push.yml @@ -5,9 +5,6 @@ permissions: on: push: - paths: - - include/** - - source/common/** branches: - main pull_request: @@ -33,9 +30,8 @@ jobs: runs-on: ubuntu-22.04 if: github.repository == 'envoyproxy/envoy' steps: - - uses: envoyproxy/toolshed/actions/bind-mounts@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 - if: | - ! github.event.repository.private + - uses: envoyproxy/toolshed/actions/bind-mounts@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 + if: ! github.event.repository.private with: mounts: | - src: /mnt/workspace @@ -44,22 +40,38 @@ jobs: - src: /mnt/runner-cache target: /home/runner/.cache chown: "runner:runner" + + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 2 + + - name: Detect C++ changes + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + git fetch --depth=1 "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" main 2>/dev/null + TO_OTHER=FETCH_HEAD + else + TO_OTHER=HEAD^1 + fi + if git diff --name-only HEAD "${TO_OTHER}" -- source/common/ include/ | grep -q .; then + echo "CPP_CHANGED=true" >> "$GITHUB_ENV" + else + echo "CPP_CHANGED=false" >> "$GITHUB_ENV" + fi + - name: Free disk space if: | - env.BUILD_TARGETS != '' + env.CPP_CHANGED == 'true' && github.event.repository.private - uses: envoyproxy/toolshed/actions/diskspace@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/diskspace@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: to_remove: | /usr/local/.ghcup /usr/local/lib/android - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 2 - - name: Get build targets + if: env.CPP_CHANGED == 'true' run: | # TODO(phlax): Shift this to an action compare_head () { @@ -67,7 +79,7 @@ jobs: if [[ -n "$line" ]]; then bazel query "rdeps($SEARCH_FOLDER, $line, 1)" 2> /dev/null fi - done < <(git diff --name-only HEAD "${1}" -- source/* include/*) + done < <(git diff --name-only HEAD "${1}" -- source/common/* include/*) } if [[ "$GIT_EVENT" == "pull_request" ]]; then git fetch "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" main 2> /dev/null @@ -83,17 +95,30 @@ jobs: GIT_EVENT: ${{ github.event_name }} - name: Set default build target - if: ${{ env.BUILD_TARGETS == '' }} + if: env.CPP_CHANGED == 'true' && env.BUILD_TARGETS == '' run: | echo "MINIMAL_BUILD_TARGET=//source/common/common:assert_lib" > $GITHUB_ENV - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # codeql-bundle-v4.35.1 + if: env.CPP_CHANGED == 'true' + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # codeql-bundle-v4.36.1 with: languages: cpp trap-caching: false + # No-op analysis to satisfy Scorecard SAST check when no C++ changes are present. + # Running codeql-action/analyze unconditionally ensures every merge commit gets + # a github-code-scanning check run with conclusion 'success', which Scorecard + # requires to award full SAST score. + - name: Initialize CodeQL (noop) + if: env.CPP_CHANGED != 'true' + uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # codeql-bundle-v4.36.1 + with: + languages: javascript + trap-caching: false + - name: Install deps + if: env.CPP_CHANGED == 'true' shell: bash run: | sudo apt-get -qq update --error-on=any @@ -103,17 +128,25 @@ jobs: # - bazel/repository_locations.bzl # - .github/workflows/codeql-daily.yml # - https://github.com/envoyproxy/envoy-build-tools/blob/main/build_container/build_container_ubuntu.sh#L84 - mkdir -p bin/clang18.1.8 - cd bin/clang18.1.8 + # + # IMPORTANT: the clang bundle MUST be unpacked outside $GITHUB_WORKSPACE (we use $RUNNER_TEMP). + # The CodeQL C++ extractor treats anything under the source root as project code, so headers + # under a checkout subdirectory would be analysed as Envoy source and produce false positives + # (e.g. cpp/new-free-mismatch in , alert #1881). Keeping it under $RUNNER_TEMP makes + # CodeQL classify those headers as system headers — same as /usr/include/c++/** on a normal + # build. Do NOT move this back into the checkout tree. + mkdir -p "${RUNNER_TEMP}/clang18.1.8" + cd "${RUNNER_TEMP}/clang18.1.8" wget -q https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/clang+llvm-18.1.8-x86_64-linux-gnu-ubuntu-18.04.tar.xz tar -xf clang+llvm-18.1.8-x86_64-linux-gnu-ubuntu-18.04.tar.xz --strip-components 1 - name: Build + if: env.CPP_CHANGED == 'true' run: | bazel shutdown bazel build \ -c fastbuild \ - --repo_env=BAZEL_LLVM_PATH="$(realpath bin/clang18.1.8)" \ + --repo_env=BAZEL_LLVM_PATH="${RUNNER_TEMP}/clang18.1.8" \ --spawn_strategy=local \ --discard_analysis_cache \ --nouse_action_cache \ @@ -124,9 +157,9 @@ jobs: echo -e "Built targets...\n${BUILD_TARGETS:-${MINIMAL_BUILD_TARGET}}" - name: Clean Artifacts + if: env.CPP_CHANGED == 'true' run: | git clean -xdf - name: Perform CodeQL Analysis - # if: ${{ env.BUILD_TARGETS != '' }} - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # codeql-bundle-v4.35.1 + uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # codeql-bundle-v4.36.1 diff --git a/.github/workflows/command.yml b/.github/workflows/command.yml index c42f30a20d442..a644b04d903f6 100644 --- a/.github/workflows/command.yml +++ b/.github/workflows/command.yml @@ -28,7 +28,7 @@ jobs: && github.actor != 'dependabot[bot]' }} steps: - - uses: envoyproxy/toolshed/actions/github/command@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/command@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Parse command from comment id: command with: @@ -37,14 +37,14 @@ jobs: ^/(retest) # /retest - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: ${{ steps.command.outputs.command == 'retest' }} id: appauth-retest name: Appauth (retest) with: key: ${{ secrets.ENVOY_CI_APP_KEY }} app_id: ${{ secrets.ENVOY_CI_APP_ID }} - - uses: envoyproxy/toolshed/actions/retest@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/retest@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 if: ${{ steps.command.outputs.command == 'retest' }} name: Retest with: diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index abd924731860c..7f3909b09840b 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -21,7 +21,7 @@ jobs: sudo rm -rf /usr/local/lib/android & sudo rm -rf /usr/share/dotnet & - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install deps shell: bash run: | diff --git a/.github/workflows/envoy-checks.yml b/.github/workflows/envoy-checks.yml index 8ba40a47499bd..75ec8c3eaac05 100644 --- a/.github/workflows/envoy-checks.yml +++ b/.github/workflows/envoy-checks.yml @@ -46,6 +46,9 @@ jobs: # head-sha: ${{ github.sha }} build: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }} permissions: actions: read contents: read @@ -62,6 +65,7 @@ jobs: coverage: secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} gcp-key: ${{ fromJSON(needs.load.outputs.trusted) && secrets.GCP_SERVICE_ACCOUNT_KEY_TRUSTED || secrets.GCP_SERVICE_ACCOUNT_KEY }} permissions: actions: read @@ -78,6 +82,8 @@ jobs: trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} runtime: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -93,6 +99,8 @@ jobs: trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} san: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/envoy-deflake.yml b/.github/workflows/envoy-deflake.yml new file mode 100644 index 0000000000000..9d547e6777e81 --- /dev/null +++ b/.github/workflows/envoy-deflake.yml @@ -0,0 +1,60 @@ +name: Envoy/deflake + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + target: + description: 'Bazel target to test' + type: string + required: true + test: + description: 'C++ test' + type: string + required: true + bazel-args: + description: Toolchain/test type + type: string + default: >- + --config=clang + runs: + description: 'Number of runs to test' + type: number + default: 1000 + + +jobs: + env: + secrets: + lock-app-key: ${{ secrets.ENVOY_CI_MUTEX_APP_KEY }} + lock-app-id: ${{ secrets.ENVOY_CI_MUTEX_APP_ID }} + permissions: + contents: read + uses: ./.github/workflows/_load_env.yml + + deflake: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + permissions: + actions: read + contents: read + packages: read + needs: env + uses: ./.github/workflows/_run.yml + with: + bazel-extra: >- + --config=rbe + ${{ inputs.bazel-args }} + rbe: true + request: ${{ needs.env.outputs.request }} + target: deflake + trusted: true + steps-pre: | + - shell: bash + run: | + echo ENVOY_DEFLAKE_JOBS=300 >> $GITHUB_ENV + echo ENVOY_DEFLAKE_RUNS=${{ inputs.runs }} >> $GITHUB_ENV + echo ENVOY_DEFLAKE_TARGET=${{ inputs.target }} >> $GITHUB_ENV + echo ENVOY_DEFLAKE_TEST=${{ inputs.test }} >> $GITHUB_ENV diff --git a/.github/workflows/envoy-dependency.yml b/.github/workflows/envoy-dependency.yml index 691ba7f760891..9e7a739032b47 100644 --- a/.github/workflows/envoy-dependency.yml +++ b/.github/workflows/envoy-dependency.yml @@ -53,16 +53,16 @@ jobs: steps: - id: appauth name: Appauth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_DEP_APP_ID }} key: ${{ secrets.ENVOY_CI_DEP_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/actions/bson@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bson@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: update name: Update dependency (${{ inputs.dependency }}) with: @@ -97,13 +97,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: envoyproxy/toolshed/actions/upload/diff@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/upload/diff@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Upload diff with: name: ${{ inputs.dependency }}-${{ steps.update.outputs.output }} - name: Create a PR if: ${{ inputs.pr }} - uses: envoyproxy/toolshed/actions/github/pr@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/pr@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: base: main body: | @@ -134,11 +134,28 @@ jobs: steps: - id: appauth name: Appauth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_DEP_APP_ID }} key: ${{ secrets.ENVOY_CI_DEP_APP_KEY }} - - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/bind-mounts@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 + if: ! github.event.repository.private + with: + mounts: | + - src: /mnt/workspace + target: GITHUB_WORKSPACE + chown: "runner:runner" + - src: /mnt/runner-cache + target: /home/runner/.cache + chown: "runner:runner" + - name: Free disk space + if: github.event.repository.private + uses: envoyproxy/toolshed/actions/diskspace@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 + with: + to_remove: | + /usr/local/.ghcup + /usr/local/lib/android + - uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: checkout name: Checkout Envoy repository with: @@ -146,12 +163,6 @@ jobs: path: envoy fetch-depth: 0 token: ${{ steps.appauth.outputs.token }} - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - name: Checkout Envoy build tools repository - with: - repository: envoyproxy/envoy-build-tools - path: build-tools - fetch-depth: 0 - run: | shas=( sha-ci @@ -171,20 +182,36 @@ jobs: working-directory: envoy - run: | if [[ -z "$CONTAINER_TAG" ]]; then - # get current build image version - CONTAINER_TAG=$(git log -1 --pretty=format:"%H" "./docker") + # Source of truth: the latest `docker-v*` GitHub release in + # envoyproxy/toolshed. Strip only the `docker-` prefix - the + # published images keep the leading `v`, eg + # `envoyproxy/envoy-build:ci-v0.1.6`. + CONTAINER_TAG=$( + gh api -H "Accept: application/vnd.github+json" \ + --paginate \ + /repos/envoyproxy/toolshed/releases \ + --jq 'map(select(.tag_name | startswith("docker-v"))) | .[0].tag_name // empty' \ + | head -n1 \ + | sed 's/^docker-//') + fi + if [[ -z "$CONTAINER_TAG" ]]; then + echo "ERROR: Could not determine build image tag from envoyproxy/toolshed releases" >&2 + exit 1 fi + # Normalize: accept `docker-v0.1.6`, `v0.1.6` or `0.1.4` from inputs.version + CONTAINER_TAG="${CONTAINER_TAG#docker-}" + [[ "$CONTAINER_TAG" == v* ]] || CONTAINER_TAG="v${CONTAINER_TAG}" echo "tag=${CONTAINER_TAG}" >> "$GITHUB_OUTPUT" - echo "tag_short=${CONTAINER_TAG::7}" >> "$GITHUB_OUTPUT" + echo "tag_short=${CONTAINER_TAG}" >> "$GITHUB_OUTPUT" env: CONTAINER_TAG: ${{ inputs.version }} + GH_TOKEN: ${{ steps.appauth.outputs.token }} id: build-tools name: Build image SHA - working-directory: build-tools - name: Check Docker SHAs id: build-images - uses: envoyproxy/toolshed/actions/docker/shas@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/docker/shas@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: images: | sha-ci: docker.io/envoyproxy/envoy-build:ci-${{ steps.build-tools.outputs.tag }} @@ -222,7 +249,7 @@ jobs: name: Update SHAs working-directory: envoy - name: Create a PR - uses: envoyproxy/toolshed/actions/github/pr@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/pr@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: base: main body: Created by Envoy dependency bot @@ -251,7 +278,7 @@ jobs: issues: write steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run dependency checker run: | TODAY_DATE=$(date -u -I"date") diff --git a/.github/workflows/envoy-macos.yml b/.github/workflows/envoy-macos.yml index 8fc8d4b9965d4..a460a4a66be92 100644 --- a/.github/workflows/envoy-macos.yml +++ b/.github/workflows/envoy-macos.yml @@ -39,6 +39,8 @@ jobs: check-name: macos macos: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/envoy-prechecks.yml b/.github/workflows/envoy-prechecks.yml index b3b0b19c0b5f1..9115f3d1bc6c9 100644 --- a/.github/workflows/envoy-prechecks.yml +++ b/.github/workflows/envoy-prechecks.yml @@ -42,6 +42,8 @@ jobs: check-name: prechecks format: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -57,6 +59,8 @@ jobs: trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} deps: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -74,6 +78,7 @@ jobs: publish: secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} gcp-key: >- ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) @@ -94,6 +99,8 @@ jobs: trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} external: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/envoy-publish.yml b/.github/workflows/envoy-publish.yml index 06cde48e512da..441dab09abf27 100644 --- a/.github/workflows/envoy-publish.yml +++ b/.github/workflows/envoy-publish.yml @@ -49,11 +49,8 @@ jobs: # head-sha: ${{ github.sha }} build: - permissions: - actions: read - contents: read - packages: read secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} gpg-key: >- ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) @@ -64,6 +61,10 @@ jobs: && fromJSON(needs.load.outputs.trusted) && secrets.ENVOY_GPG_MAINTAINER_KEY_PASSWORD || secrets.ENVOY_GPG_SNAKEOIL_KEY_PASSWORD }} + permissions: + actions: read + contents: read + packages: read if: ${{ fromJSON(needs.load.outputs.request).run.release || fromJSON(needs.load.outputs.request).run.verify }} needs: - load @@ -82,8 +83,8 @@ jobs: release: secrets: - dockerhub-password: ${{ secrets.DOCKERHUB_PASSWORD }} - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-write-token: ${{ secrets.DOCKERHUB_WRITE_TOKEN }} ENVOY_CI_SYNC_APP_ID: >- ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) @@ -125,10 +126,13 @@ jobs: uses: ./.github/workflows/_publish_release.yml name: Release with: + dockerhub-username: ${{ vars.DOCKERHUB_USERNAME }} request: ${{ needs.load.outputs.request }} trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} verify: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/envoy-release.yml b/.github/workflows/envoy-release.yml index fd6be0dc4530e..ac920e3bf0292 100644 --- a/.github/workflows/envoy-release.yml +++ b/.github/workflows/envoy-release.yml @@ -60,14 +60,14 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} @@ -88,10 +88,10 @@ jobs: name: Check changelog summary - if: ${{ inputs.author }} name: Validate signoff email - uses: envoyproxy/toolshed/actions/email/validate@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/email/validate@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: email: ${{ inputs.author }} - - uses: envoyproxy/toolshed/actions/github/run@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/run@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Create release with: source: | @@ -116,7 +116,7 @@ jobs: name: Release version id: release - name: Create a PR - uses: envoyproxy/toolshed/actions/github/pr@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/pr@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: base: ${{ github.ref_name }} commit: false @@ -142,19 +142,19 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} strip-prefix: release/ token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/actions/github/run@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/run@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Re-open branch with: command: >- @@ -169,7 +169,7 @@ jobs: name: Dev version id: dev - name: Create a PR - uses: envoyproxy/toolshed/actions/github/pr@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/pr@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: base: ${{ github.ref_name }} commit: false @@ -193,20 +193,20 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} strip-prefix: release/ token: ${{ steps.appauth.outputs.token }} - - uses: envoyproxy/toolshed/actions/github/run@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/github/run@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 name: Sync version histories with: command: >- @@ -216,7 +216,7 @@ jobs: -- --signoff="${{ env.COMMITTER_NAME }} <${{ env.COMMITTER_EMAIL }}>" - name: Create a PR - uses: envoyproxy/toolshed/actions/github/pr@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/pr@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: append-commit-message: true base: ${{ github.ref_name }} @@ -243,13 +243,13 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - id: checkout name: Checkout Envoy repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: config: | fetch-depth: 0 @@ -279,13 +279,13 @@ jobs: steps: - id: appauth name: App auth - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: app_id: ${{ secrets.ENVOY_CI_PUBLISH_APP_ID }} key: ${{ secrets.ENVOY_CI_PUBLISH_APP_KEY }} - name: Checkout repository - uses: envoyproxy/toolshed/actions/github/checkout@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + uses: envoyproxy/toolshed/actions/github/checkout@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: committer-name: ${{ env.COMMITTER_NAME }} committer-email: ${{ env.COMMITTER_EMAIL }} diff --git a/.github/workflows/envoy-security-check.yml b/.github/workflows/envoy-security-check.yml index 958bc5f337847..1839f8e0687c4 100644 --- a/.github/workflows/envoy-security-check.yml +++ b/.github/workflows/envoy-security-check.yml @@ -68,7 +68,7 @@ jobs: # PR - name: Comment on PR if: matrix.action == 'comment' && github.event.workflow_run.pull_requests[0] - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | try { @@ -101,7 +101,7 @@ jobs: # SLACK - name: Checkout repository (secure branch) if: matrix.action == 'slack' - uses: actions/checkout@v6 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: # Explicitly checkout main to avoid malicious code ref: main diff --git a/.github/workflows/envoy-sync.yml b/.github/workflows/envoy-sync.yml index c780f59df734d..75e6b2f25a19a 100644 --- a/.github/workflows/envoy-sync.yml +++ b/.github/workflows/envoy-sync.yml @@ -11,33 +11,44 @@ on: concurrency: group: ${{ github.workflow }} - cancel-in-progress: true + jobs: sync: runs-on: ubuntu-24.04 if: >- - ${{ - github.repository == 'envoyproxy/envoy' - && (github.ref_name == 'main') - && (github.event.push - || !contains(github.actor, '[bot]')) - }} + github.repository == 'envoyproxy/envoy' + && (github.ref_name == 'main') + && (github.event.push + || !contains(github.actor, '[bot]')) strategy: fail-fast: false matrix: downstream: - go-control-plane - - envoy-filter-example - data-plane-api - mobile-website steps: - - uses: envoyproxy/toolshed/actions/appauth@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - name: Skip if superseded by a newer commit on main + id: tip + env: + GH_TOKEN: ${{ github.token }} + run: | + TIP=$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha) + echo "Tip of main: ${TIP}" + echo "This commit: ${GITHUB_SHA}" + if [ "${TIP}" != "${GITHUB_SHA}" ]; then + echo "Superseded by ${TIP} - skipping sync of ${{ matrix.downstream }}." + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + - if: steps.tip.outputs.skip != 'true' + uses: envoyproxy/toolshed/actions/appauth@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 id: appauth with: app_id: ${{ secrets.ENVOY_CI_SYNC_APP_ID }} key: ${{ secrets.ENVOY_CI_SYNC_APP_KEY }} - - uses: envoyproxy/toolshed/actions/dispatch@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - if: steps.tip.outputs.skip != 'true' + uses: envoyproxy/toolshed/actions/dispatch@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: repository: "envoyproxy/${{ matrix.downstream }}" ref: main diff --git a/.github/workflows/mobile-android_build.yml b/.github/workflows/mobile-android_build.yml index 55696ada7559f..32ddcb1d7d3db 100644 --- a/.github/workflows/mobile-android_build.yml +++ b/.github/workflows/mobile-android_build.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-android build: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -63,6 +65,8 @@ jobs: target: build kotlin-hello-world: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -103,9 +107,9 @@ jobs: target: kotlin-hello-world runs-on: ubuntu-22.04 steps-pre: | - - uses: envoyproxy/toolshed/actions/envoy/android/pre@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/pre@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 steps-post: | - - uses: envoyproxy/toolshed/actions/envoy/android/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: apk: bazel-bin/examples/kotlin/hello_world/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoykotlin/.MainActivity @@ -115,6 +119,8 @@ jobs: working-directory: mobile apps: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -146,7 +152,7 @@ jobs: target: ${{ matrix.target }} runs-on: ubuntu-22.04 steps-pre: | - - uses: envoyproxy/toolshed/actions/envoy/android/pre@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/pre@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 steps-post: ${{ matrix.steps-post }} timeout-minutes: 50 trusted: ${{ needs.load.outputs.trusted && fromJSON(needs.load.outputs.trusted) || false }} @@ -157,7 +163,7 @@ jobs: include: - name: java-hello-world steps-post: | - - uses: envoyproxy/toolshed/actions/envoy/android/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: apk: bazel-bin/examples/java/hello_world/hello_envoy.apk app: io.envoyproxy.envoymobile.helloenvoy/.MainActivity @@ -180,7 +186,7 @@ jobs: --config=mobile-android-release //test/kotlin/apps/baseline:hello_envoy_kt steps-post: | - - uses: envoyproxy/toolshed/actions/envoy/android/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: apk: bazel-bin/test/kotlin/apps/baseline/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoybaselinetest/.MainActivity @@ -197,7 +203,7 @@ jobs: --config=mobile-android-release //test/kotlin/apps/experimental:hello_envoy_kt steps-post: | - - uses: envoyproxy/toolshed/actions/envoy/android/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + - uses: envoyproxy/toolshed/actions/envoy/android/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 with: apk: bazel-bin/test/kotlin/apps/experimental/hello_envoy_kt.apk app: io.envoyproxy.envoymobile.helloenvoyexperimentaltest/.MainActivity diff --git a/.github/workflows/mobile-android_tests.yml b/.github/workflows/mobile-android_tests.yml index 67b48f624a808..efef5a0953fef 100644 --- a/.github/workflows/mobile-android_tests.yml +++ b/.github/workflows/mobile-android_tests.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-android-tests linux: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-asan.yml b/.github/workflows/mobile-asan.yml index 81dffcf3d8f1b..d846b415f1dc0 100644 --- a/.github/workflows/mobile-asan.yml +++ b/.github/workflows/mobile-asan.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-asan asan: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-cc_tests.yml b/.github/workflows/mobile-cc_tests.yml index 9f92b15051735..9bdc0a7cb0972 100644 --- a/.github/workflows/mobile-cc_tests.yml +++ b/.github/workflows/mobile-cc_tests.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-cc cc-tests: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -59,6 +61,8 @@ jobs: target: cc-tests cc-xds-integration-tests: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-coverage.yml b/.github/workflows/mobile-coverage.yml index 5a20cda9ec4f0..4c46407b6a09d 100644 --- a/.github/workflows/mobile-coverage.yml +++ b/.github/workflows/mobile-coverage.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-coverage coverage: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-docs.yml b/.github/workflows/mobile-docs.yml index f044859efa54a..0ca823198e3dd 100644 --- a/.github/workflows/mobile-docs.yml +++ b/.github/workflows/mobile-docs.yml @@ -40,6 +40,7 @@ jobs: docs: secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} ssh-key-extra: ${{ needs.load.outputs.trusted && secrets.ENVOY_MOBILE_WEBSITE_DEPLOY_KEY || '' }} permissions: actions: read diff --git a/.github/workflows/mobile-format.yml b/.github/workflows/mobile-format.yml index e259bf1b3c744..fe88f65f8aee7 100644 --- a/.github/workflows/mobile-format.yml +++ b/.github/workflows/mobile-format.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-format container: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-ios_build.yml b/.github/workflows/mobile-ios_build.yml index d717cda047cc5..0aaec3167529a 100644 --- a/.github/workflows/mobile-ios_build.yml +++ b/.github/workflows/mobile-ios_build.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-ios build: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -101,7 +103,7 @@ jobs: # source ./ci/mac_ci_setup.sh # bazel shutdown # steps-post: | - # - uses: envoyproxy/toolshed/actions/envoy/ios/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + # - uses: envoyproxy/toolshed/actions/envoy/ios/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 # with: # app: ${{ matrix.app }} # args: ${{ matrix.args || '--config=mobile-ios' }} @@ -146,7 +148,7 @@ jobs: # source: | # source ./ci/mac_ci_setup.sh # steps-post: | - # - uses: envoyproxy/toolshed/actions/envoy/ios/post@8d5d8d4b9eeb5e4e76b92341b0b1b1f6438af231 # v0.4.5 + # - uses: envoyproxy/toolshed/actions/envoy/ios/post@1f5b552c6749502b885cb8cf23549b929e745547 # actions-v0.4.17 # with: # app: ${{ matrix.app }} # args: ${{ matrix.args || '--config=mobile-ios' }} diff --git a/.github/workflows/mobile-ios_tests.yml b/.github/workflows/mobile-ios_tests.yml index 3556c94686f78..0c4ef8b6ebe48 100644 --- a/.github/workflows/mobile-ios_tests.yml +++ b/.github/workflows/mobile-ios_tests.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-ios-tests tests: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-perf.yml b/.github/workflows/mobile-perf.yml index ba63d77844e3a..28edb20beb7f4 100644 --- a/.github/workflows/mobile-perf.yml +++ b/.github/workflows/mobile-perf.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-perf build: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read @@ -85,6 +87,8 @@ jobs: target: size-main compare: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-python.yml b/.github/workflows/mobile-python.yml index 034bf28434949..adb8945eece2b 100644 --- a/.github/workflows/mobile-python.yml +++ b/.github/workflows/mobile-python.yml @@ -39,6 +39,8 @@ jobs: check-name: mobile-python python-tests: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 46e7248d36797..3f26913436566 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -5,6 +5,16 @@ permissions: on: workflow_dispatch: + inputs: + job_to_run: + description: 'Select the job to run' + required: true + default: 'all' + type: choice + options: + - all + - android + - python schedule: # Mondays at 1pm UTC (8am EST) - cron: "0 13 * * 1" @@ -18,16 +28,20 @@ jobs: contents: read uses: ./.github/workflows/_load_env.yml - release: + android-release: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: + actions: read contents: read packages: read if: >- - ${{ - github.repository == 'envoyproxy/envoy' - && (github.event.schedule - || !contains(github.actor, '[bot]')) - }} + github.repository == 'envoyproxy/envoy' + && (github.event.schedule + || !contains(github.actor, '[bot]')) + && (github.event.inputs.job_to_run == 'all' + || github.event.inputs.job_to_run == 'android' + || github.event_name == 'schedule') needs: env uses: ./.github/workflows/_mobile_container_ci.yml with: @@ -76,8 +90,14 @@ jobs: //:android_dist output: envoy - deploy: - needs: release + android-deploy: + needs: android-release + if: >- + always() + && (github.event.inputs.job_to_run == 'all' + || github.event.inputs.job_to_run == 'android' + || github.event_name == 'schedule') + && (needs.android-release.result == 'success') permissions: contents: read packages: read @@ -89,7 +109,7 @@ jobs: include: - output: envoy steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Add safe directory @@ -141,3 +161,83 @@ jobs: ${{ matrix.output }}-pom.xml.asc \ ${{ matrix.output }}-sources.jar.asc \ ${{ matrix.output }}-javadoc.jar.asc + + python-release: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} + permissions: + actions: read + contents: read + packages: read + if: >- + github.repository == 'envoyproxy/envoy' + && (github.event_name == 'workflow_dispatch') + && (github.event.inputs.job_to_run == 'all' + || github.event.inputs.job_to_run == 'python') + needs: env + name: Build and Publish Python Wheel + uses: ./.github/workflows/_mobile_container_ci.yml + with: + args: ${{ matrix.args }} + container: ${{ fromJSON(needs.env.outputs.build-image).default }} + container-output: | + "bazel-bin/library/python/": build/ + request: ${{ needs.env.outputs.request }} + steps-post: | + - name: Repair wheel + run: | + mkdir -p /tmp/dist + WHEEL_PATH=$(find /tmp/container-output/build -name "*.whl" | head -n 1) + + if [[ -z "$WHEEL_PATH" ]]; then + echo "Error: No wheel file found in /tmp/container-output/build" + ls -R /tmp/container-output/ + exit 1 + fi + python3 -m venv envoy_wheel + source envoy_wheel/bin/activate + pip install patchelf auditwheel + + auditwheel repair "$WHEEL_PATH" --wheel-dir /tmp/dist + shell: bash + target: ${{ matrix.target }} + upload-name: python-wheel + upload-path: /tmp/dist/*.whl + strategy: + fail-fast: false + matrix: + include: + - target: envoy_mobile_wheel + args: >- + build + --config=ci + --config=mobile-rbe + --config=mobile-release + -c opt --strip=always + //library/python:envoy_mobile_wheel + --//library/python:python_platform="manylinux2014_x86_64" + + python-publish: + needs: python-release + if: >- + always() + && (github.event.inputs.job_to_run == 'all' + || github.event.inputs.job_to_run == 'python') + && (needs.python-release.result == 'success') + permissions: + contents: read + id-token: write + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + environment: + name: pypi + url: https://pypi.org/p/envoy-mobile-client + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-wheel + path: dist/ + - name: Publish to PyPi + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1.14 diff --git a/.github/workflows/mobile-tsan.yml b/.github/workflows/mobile-tsan.yml index 04aed68674ef1..0e8cf77253804 100644 --- a/.github/workflows/mobile-tsan.yml +++ b/.github/workflows/mobile-tsan.yml @@ -40,6 +40,8 @@ jobs: run-id: ${{ github.event.workflow_run.id }} tsan: + secrets: + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} permissions: actions: read contents: read diff --git a/.github/workflows/pr_notifier.yml b/.github/workflows/pr_notifier.yml index 2f01a2e829b87..fb4d1160b5517 100644 --- a/.github/workflows/pr_notifier.yml +++ b/.github/workflows/pr_notifier.yml @@ -24,7 +24,7 @@ jobs: || !contains(github.actor, '[bot]')) }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Notify about PRs run: | ARGS=() diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8fe6db29973a7..e9b7c3906973e 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -22,7 +22,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false @@ -41,6 +41,6 @@ jobs: retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v4.36.1 with: sarif_file: results.sarif diff --git a/.github/workflows/toolchain-test.yml b/.github/workflows/toolchain-test.yml index bfbc731637e00..b25d12e1d7766 100644 --- a/.github/workflows/toolchain-test.yml +++ b/.github/workflows/toolchain-test.yml @@ -32,7 +32,7 @@ jobs: name: "Test: ${{ matrix.name }}" steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run matrix test run: | cd ci/matrix diff --git a/.gitignore b/.gitignore index fb40cbde2890d..06dcce55c4b87 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ node_modules # Rust **/rust/**/target +target ############################# # OS/editor cruft diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000000..cd02017982972 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,235 @@ +# AGENTS.md + +Instructions for AI coding agents (Claude Code, Copilot, Cursor, etc.) working in this repository. + +## Critical rules + +1. **Always sign off commits.** The human user must sign off commits via `git commit -s` — never + manually write a `Signed-off-by` trailer. The sign-off attests that the committer (the user) + has the right to submit the code under the project's license. +2. **Always run format and lint checks before committing.** Use `tools/local_fix_format.sh` for + a quick local check, or run `./ci/do_ci.sh format` inside Docker for the full CI check suite. + Format failures are the most common CI rejection. If running checks is impractical, warn the + user that formatting has not been verified. +3. **Never amend commits or force-push after a PR has received human review.** Always create new + commits to preserve review history. +4. **Never rebase a PR that is under review.** Use `git merge main` instead to pull in recent + changes. The project squash-merges, so commit count does not matter. +5. **Disclose AI usage.** When submitting PRs, include a note about AI assistance in the PR + description. The submitter must fully understand all code being submitted. +6. **Never commit to `main`.** Always create a new branch before committing. If switching + contexts or unsure which branch to use, ask the user. +7. **Always push to a personal fork.** Do not create branches in the main repo. + +## Developer workflow + +### 1. Before starting work + +Read `CONTRIBUTING.md` for the full contribution process. Key points: +- **Major features (>100 LOC or user-facing):** Open a GitHub issue first to discuss design. + For new extensions, read `EXTENSION_POLICY.md`. +- **Small patches and bug fixes:** No prior communication needed. +- Install git hooks: `./support/bootstrap` + +### 2. Writing code + +Read `STYLE.md` for the C++ coding style. After writing C++ code, run `clang-format` to fix +formatting automatically rather than trying to hand-format: + +```bash +clang-format -i +``` + +Tests must: +- Live in `test/` mirroring the `source/` structure +- Achieve 100% coverage for new code +- Use `StrictMock` by default, `SimulatedTimeSystem` for time, port 0 for network +- Unit tests must be hermetic and deterministic — no real time, no randomness +- Integration tests (in `test/integration/`) use real network on localhost + +### 3. Building and testing + +See `bazel/README.md` for full build documentation. Common commands: + +```bash +# Docker-based (recommended — matches CI environment) +./ci/run_envoy_docker.sh bash # interactive shell +./ci/do_ci.sh debug //test/common/http/... # build + test +./ci/do_ci.sh debug.server_only # build binary only + +# Local (requires local dependencies) +bazel test -c dbg //test/common/http/... # run tests +bazel build --config=clang -c opt //source/exe:envoy-static # optimized binary +``` + +Sanitizers, coverage, GDB debugging, and profiling are resource-intensive. Do **not** run +them unless the user explicitly asks. See `bazel/README.md` and `bazel/PPROF.md`. + +### 4. Format and lint checks (required before every commit) + +Format failures are the most common CI rejection. Agents should produce content that conforms +to the repo's style conventions for all file types (C++, BUILD, YAML, Markdown, shell, etc.). + +**Quick local check (recommended):** + +```bash +tools/local_fix_format.sh # uncommitted changes (default) +tools/local_fix_format.sh -main # changes since main +tools/local_fix_format.sh -all # entire repo +``` + +**Individual checks:** + +```bash +bazel run //tools/code_format:check_format -- fix # C++, BUILD, .bzl, .proto +bazel run //tools/spelling:check_spelling_pedantic -- fix # spelling +./ci/do_ci.sh format # full CI check (inside Docker) +``` + +**Linter config files — read these to produce compliant output without running the tools:** + +| Config file | What it configures | +|-------------|--------------------| +| `.clang-format` | C++/Proto formatting (100-col, include order, pointer alignment) | +| `.yamllint` | YAML rules (140-col max, consistent indentation) | +| `.flake8` | Python lint rules | +| `rustfmt.toml` | Rust formatting (100-col, 2-space indent) | +| `tools/spelling/spelling_dictionary.txt` | Custom word list (1700+ project terms) | + +### 5. Creating the commit + +Before your first commit in a session: + +1. Check if the local `main` is up to date with `origin/main`: + ```bash + git fetch origin + git log main..origin/main --oneline + ``` +2. If `main` is behind, sync it: + ```bash + git checkout main && git pull && git checkout - + ``` +3. Create a new branch off the updated `main`: + ```bash + git checkout -b + ``` +4. If you had uncommitted changes that conflict with the updated `main`, ask the user whether + to proceed on the outdated base or resolve conflicts against the new `main`. + +If you're switching contexts or unsure which branch to commit to, ask the user before committing. + +```bash +git add +git commit -s # -s adds Signed-off-by automatically; NEVER write it manually +``` + +### 6. Pushing and creating a PR + +**PR title format** — lower-case subsystem prefix followed by a colon: +`docs: fix grammar error`, `router: add x-envoy-overloaded header` + +**PR description template** — every PR must fill in: + +``` +Commit Message: +Additional Description: +Risk Level: Low | Medium | High +Testing: +Docs Changes: +Release Notes: +``` + +See `PULL_REQUESTS.md` for full field descriptions and optional fields (runtime guard, +deprecation, platform-specific features). + +**Release notes:** User-facing changes **must** add a release note fragment under +`changelogs/current/`. Name the file `__.rst`. + +### 7. Waiting for CI and review + +- Do **not** create draft PRs if you want prompt reviews — draft PRs are not triaged. +- To re-run failed CI tasks, add a `/retest` comment on the PR. +- PRs with no activity for 14+ days may be closed. + +### 8. Addressing review comments + +- **Never amend or force-push** after a reviewer has looked at the PR. Create new commits. +- **Never rebase.** If you need to incorporate upstream changes: + ```bash + git fetch origin main && git merge origin/main + ``` +- If the reviewer asked for a runtime guard, add one (see `CONTRIBUTING.md`). + +### 9. After merge + +The project squash-merges PRs. The "Commit Message" field in your PR description becomes the +final commit message. Make sure it's up to date before merge. + +## Understanding CI + +Envoy uses a checks-based CI system. Results appear as **GitHub Check Runs** on PRs, not as +simple workflow pass/fail statuses. + +**CI pipeline:** + +1. **`Envoy/Prechecks`** — fast checks: format/lint/spelling, dependency validation, docs build +2. **`Envoy/Checks`** — heavier checks: compilation, tests, coverage, sanitizers + +**Checking CI status:** + +```bash +gh pr checks +gh run view --log-failed +``` + +When CI fails, check the failed check run name to determine which `do_ci.sh` target to +reproduce locally. Format failures come from `Envoy/Prechecks`; build/test failures come +from `Envoy/Checks`. + +## Inclusive language + +The following terms are **not allowed**: +- ~~whitelist~~ -> allowlist +- ~~blacklist~~ -> denylist / blocklist +- ~~master~~ -> primary / main +- ~~slave~~ -> secondary / replica + +## BUILD file conventions + +See `bazel/DEVELOPER.md` for full BUILD file rules. Key points: +- Use `envoy_cc_library`, `envoy_cc_test`, `envoy_cc_mock` (not raw `cc_library`) +- Target suffixes: `_lib`, `_test`, `_mocks`, `_interface` +- Every `#include` must have a corresponding `deps` entry + +## Updating dependencies + +See `bazel/EXTERNAL_DEPS.md` and `DEPENDENCY_POLICY.md`. When updating a version: +1. Update version, sha256, and urls in `bazel/repository_locations.bzl` +2. Update `release_date` in `bazel/deps.yaml` to the UTC date of the new release +3. Prefer maintainer-provided tarballs over GitHub auto-generated ones + +## CI and GitHub Actions (for workflow file authors) + +- In `if:` conditions, do **not** wrap expressions in `${{ }}` — the `if` field evaluates + expressions implicitly. Use `${{ }}` only in string contexts (`run:`, `with:`, `env:`). +- Workflow files in `.github/workflows/` are shared across all branches (main and stable release + branches). Do not remove variables or inputs still referenced by stable branches. + +## Key files + +| File | Purpose | +|------|---------| +| `STYLE.md` | C++ coding style and error handling | +| `CONTRIBUTING.md` | Contribution guidelines, deprecation, breaking changes | +| `PULL_REQUESTS.md` | PR field descriptions | +| `EXTENSION_POLICY.md` | Extension lifecycle and requirements | +| `DEPENDENCY_POLICY.md` | External dependency rules | +| `RELEASES.md` | Release schedule and backport process | +| `SECURITY.md` | Security reporting and disclosure | +| `REPO_LAYOUT.md` | Repository structure | +| `bazel/README.md` | Building, testing, sanitizers, coverage | +| `bazel/DEVELOPER.md` | BUILD file conventions | +| `bazel/EXTERNAL_DEPS.md` | Managing external dependencies | +| `bazel/PPROF.md` | Performance profiling | +| `source/extensions/extensions_metadata.yaml` | Extension status and security posture | +| `source/common/runtime/runtime_features.cc` | Runtime feature flag defaults | diff --git a/CODEOWNERS b/CODEOWNERS index e12d493291cfc..30c765b962fbe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -105,6 +105,7 @@ extensions/filters/common/original_src @klarose @mattklein123 /*/extensions/common/dynamic_forward_proxy @mattklein123 @ryantheoptimist /*/extensions/filters/http/dynamic_forward_proxy @mattklein123 @ryantheoptimist /*/extensions/filters/http/composite @mattklein123 @tyxia @yanavlasov +/*/extensions/filters/http/filter_chain @wbpcode @agrawroh @tyxia # omit_canary_hosts retry predicate /*/extensions/retry/host/omit_canary_hosts @sriduth @mattklein123 # previous hosts @@ -156,10 +157,11 @@ extensions/filters/common/original_src @klarose @mattklein123 /*/extensions/grpc_credentials/file_based_metadata @wozz @yanavlasov /*/extensions/internal_redirect @botengyao @penguingao /*/extensions/stat_sinks/dog_statsd @taiki45 @paul-r-gall +/*/extensions/stat_sinks/dynamic_modules @prashanthjos @mathetake @agrawroh @wbpcode /*/extensions/stat_sinks/graphite_statsd @vaccarium @mattklein123 /*/extensions/stat_sinks/hystrix @trabetti @paul-r-gall /*/extensions/stat_sinks/metrics_service @ramaraochavali @paul-r-gall -/*/extensions/stat_sinks/open_telemetry @ohadvano @mattklein123 +/*/extensions/stat_sinks/open_telemetry @ohadvano @mattklein123 @kyessenov # webassembly stat-sink extensions /*/extensions/stat_sinks/wasm @mpwarres @kyessenov @lizan /*/extensions/resource_monitors/injected_resource @eziskind @yanavlasov @@ -172,6 +174,7 @@ extensions/filters/common/original_src @klarose @mattklein123 /*/extensions/retry/priority/previous_priorities @ravenblackx @mattklein123 /*/extensions/retry/host @ravenblackx @mattklein123 /*/extensions/filters/network/http_connection_manager @yanavlasov @mattklein123 +/*/extensions/filters/network/tcp_bandwidth_limit @yanavlasov @nezdolik @antonkanug /*/extensions/filters/network/tcp_proxy @zuercher @ggreenway /*/extensions/filters/network/echo @yanavlasov @adisuissa /*/extensions/filters/udp/dns_filter @mattklein123 @yanjunxiang-google @@ -246,7 +249,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 # HTTP MCP filter /*/extensions/filters/http/mcp @botengyao @yanavlasov @wdauchy # HTTP MCP Jon Rest Bridge filter -/*/extensions/filters/http/mcp_json_rest_bridge @paulhong01 @leon-gg @botengyao @tyxia @yanavlasov +/*/extensions/filters/http/mcp_json_rest_bridge @paulhong01 @leon-gg @botengyao @tyxia @yanavlasov @guoyilin42 # MCP router filter /*/extensions/filters/http/mcp_router @botengyao @yanavlasov @wdauchy @agrawroh # HTTP A2A filter @@ -263,6 +266,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/filters/http/set_metadata @aguinet @mattklein123 # Formatters /*/extensions/formatter/cel @kyessenov @zirain +/*/extensions/formatter/dynamic_modules @agrawroh @wbpcode @mathetake /*/extensions/formatter/file_content @ggreenway @wbpcode /*/extensions/formatter/generic_secret @ggreenway @wbpcode /*/extensions/formatter/metadata @cpakulski @ravenblackx @nezdolik @@ -286,6 +290,8 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/network/dns_resolver/apple @yanavlasov @mattklein123 /*/extensions/network/dns_resolver/getaddrinfo @fredyw @mattklein123 /*/extensions/network/dns_resolver/hickory @agrawroh @yanavlasov @wbpcode +# sockmap socket interface +/*/extensions/network/socket_interface/sockmap/ @agrawroh @wbpcode @yanavlasov # compression code /*/extensions/filters/http/decompressor @kbaichoo @mattklein123 /*/extensions/filters/http/compressor @kbaichoo @mattklein123 @@ -302,11 +308,12 @@ extensions/upstreams/tcp @ggreenway @mattklein123 # health check /*/extensions/filters/http/health_check @mattklein123 @adisuissa # lua -/*/extensions/filters/http/lua @mattklein123 @wbpcode -/*/extensions/filters/common/lua @mattklein123 @wbpcode +/*/extensions/filters/http/lua @mattklein123 @wbpcode @derekargueta +/*/extensions/filters/common/lua @mattklein123 @wbpcode @derekargueta # rbac /*/extensions/filters/network/rbac @yangminzhu @yanavlasov /*/extensions/filters/http/rbac @yangminzhu @yanavlasov +/*/extensions/filters/http/upstream_rbac @yangminzhu @yanavlasov @fl0Lec /*/extensions/filters/common/rbac @yangminzhu @yanavlasov # tap /*/extensions/filters/http/tap @mattklein123 @xu1zhou @@ -335,9 +342,9 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/tracers/zipkin @wbpcode @Shikugawa @basvanbeek /*/extensions/tracers/common @wbpcode @Shikugawa @basvanbeek # ext_authz -/*/extensions/filters/common/ext_authz @esmet @tyxia @ggreenway @antoniovleonti -/*/extensions/filters/http/ext_authz @esmet @tyxia @ggreenway @antoniovleonti -/*/extensions/filters/network/ext_authz @esmet @tyxia @ggreenway @antoniovleonti +/*/extensions/filters/common/ext_authz @esmet @tyxia @ggreenway @antoniovleonti @yanjunxiang-google +/*/extensions/filters/http/ext_authz @esmet @tyxia @ggreenway @antoniovleonti @yanjunxiang-google +/*/extensions/filters/network/ext_authz @esmet @tyxia @ggreenway @antoniovleonti @yanjunxiang-google # original dst /*/extensions/filters/listener/original_dst @kyessenov @cpakulski @lambdai @nezdolik # mongo proxy @@ -354,7 +361,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 # Thrift to metadata /*/extensions/filters/http/thrift_to_metadata @JuniorHsu @zuercher # IP tagging -/*/extensions/filters/http/ip_tagging @zuercher @JuniorHsu +/*/extensions/filters/http/ip_tagging @zuercher @JuniorHsu @nezdolik # Header to metadata /*/extensions/filters/http/header_to_metadata @zuercher @JuniorHsu # Json to metadata @@ -387,6 +394,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/load_balancing_policies/client_side_weighted_round_robin @wbpcode @adisuissa @efimki /*/extensions/load_balancing_policies/override_host @yanavlasov @tonya11en /*/extensions/load_balancing_policies/wrr_locality @wbpcode @adisuissa @efimki +/*/extensions/load_balancing_policies/load_aware_locality @tonya11en @jukie @nezdolik # Early header mutation /*/extensions/http/early_header_mutation/header_mutation @wbpcode @tyxia # Network matching extensions @@ -428,8 +436,10 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/matching/input_matchers/dynamic_modules @agrawroh @mathetake @wbpcode /*/extensions/matching/http/dynamic_modules @agrawroh @mathetake @wbpcode /*/extensions/transport_sockets/tls/cert_validator/dynamic_modules @agrawroh @mathetake @wbpcode +/*/extensions/transport_sockets/dynamic_modules @agrawroh @mathetake @wbpcode /*/extensions/upstreams/http/dynamic_modules @agrawroh @mathetake @wbpcode /*/extensions/tracers/dynamic_modules @agrawroh @mathetake @wbpcode +/*/extensions/health_checkers/dynamic_modules @basundhara-c @agrawroh @mathetake @wbpcode # Linux network namespace override /*/extensions/local_address_selectors/filter_state_override @tonya11en @kyessenov @@ -449,7 +459,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /*/extensions/filters/network/common @UNOWNED @UNOWNED /*/extensions/clusters/static @UNOWNED @UNOWNED /*/extensions/clusters/strict_dns @UNOWNED @UNOWNED -/*/extensions/clusters/original_dst @UNOWNED @UNOWNED +/*/extensions/clusters/original_dst @nezdolik @tonya11en @jronak /*/extensions/clusters/logical_dns/ @UNOWNED @UNOWNED /*/extensions/clusters/common/ @UNOWNED @UNOWNED /*/extensions/clusters/eds/ @UNOWNED @UNOWNED @@ -476,6 +486,7 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /contrib/dynamo/ @UNOWNED @UNOWNED /contrib/golang/ @doujiang24 @wangfakang @StarryVae @spacewander @antJack /contrib/kafka/ @mattklein123 @adamkotwasinski +/contrib/stat_sinks/wasm_filter/ @prashanthjos @UNOWNED /contrib/rocketmq_proxy/ @aaron-ai @lizhanhui /contrib/mysql_proxy/ @rshriram @venilnoronha /contrib/postgres/protocol/ @agrawroh @cpakulski @jarupatj @@ -495,5 +506,6 @@ extensions/upstreams/tcp @ggreenway @mattklein123 /contrib/peak_ewma/load_balancing_policies/ @rroblak @UNOWNED /contrib/kae/ @Misakokoro @UNOWNED /contrib/istio @kyessenov @wbpcode @keithmattix @krinkinmu @zirain +/contrib/reverse_tunnel_reporter @agrawroh @aakugan @basundhara-c /compat/openssl/ @tedjpoole @envoyproxy/envoy-openssl-sync diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3ce7e0018d63..9a7c56037b416 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,6 +131,7 @@ versioning guidelines: [envoy-announce](https://groups.google.com/forum/#!forum/envoy-announce) email list but by default it is expected the multi-phase warn-by-default/fail-by-default is sufficient to warn users to move away from deprecated features. +* Given their criticality, changing the default behavior of the ext_authz and ext_proc protocols is strictly forbidden. # Submitting a PR @@ -151,11 +152,15 @@ versioning guidelines: build. If your PR cannot have 100% coverage for some reason please clearly explain why when you open it. * Any PR that changes user-facing behavior **must** have associated documentation in [docs](docs) as - well as [release notes](changelogs/current.yaml). API changes should be documented - inline with protos as per the [API contribution guidelines](api/CONTRIBUTING.md). If a change applies - to multiple sections of the release notes, it should be noted in the first (most important) section - that applies. For instance, a bug fix that introduces incompatible behavior should be noted in - `Incompatible Behavior Changes` but not in `Bug Fixes`. + well as a release note fragment in [changelogs/current](changelogs/current). Add the fragment to + the most appropriate section directory, using the canonical sections and areas listed in + [changelogs/changelogs.yaml](changelogs/changelogs.yaml). Fragment filenames should use the form + `__.rst`, for example + `http__added-new-request-stat.rst`. API changes should be documented inline with protos as per + the [API contribution guidelines](api/CONTRIBUTING.md). If a change applies to multiple sections + of the release notes, it should be noted in the first (most important) section that applies. For + instance, a bug fix that introduces incompatible behavior should be noted in + `behavior_changes` but not in `bug_fixes`. * All code comments and documentation are expected to have proper English grammar and punctuation. If you are not a fluent English speaker (or a bad writer ;-)) please let us know and we will try to find some help but there are no guarantees. @@ -192,8 +197,9 @@ versioning guidelines: changes for 7 days. Obviously PRs that are closed due to lack of activity can be reopened later. Closing stale PRs helps us to keep on top of all of the work currently in flight. * If a commit deprecates a feature, the commit message must mention what has been deprecated. - Additionally, the [version history](changelogs/current.yaml) must be updated with - relevant RST links for fields and messages as part of the commit. + Additionally, add a release note fragment under the `deprecated` section in + [changelogs/current](changelogs/current), with relevant RST links for fields and messages as part + of the commit. * Please consider joining the [envoy-dev](https://groups.google.com/forum/#!forum/envoy-dev) mailing list. * If your PR involves any changes to @@ -265,12 +271,14 @@ Runtime code is held to the same standard as regular Envoy code, so both the old path and the new should have 100% coverage both with the feature defaulting true and false. -Please note that if adding a runtime guarded feature, your [release notes](changelogs/current.yaml) should include both the functional change, and how to revert it, for example +Please note that if adding a runtime guarded feature, your release note fragment in +[changelogs/current](changelogs/current) should include both the functional change, and how to +revert it, for example in `changelogs/current/behavior_changes/http__header-validation.rst`: -```yaml -- area: config - change: | - type URL is used to lookup extensions regardless of the name field. This may cause problems for empty filter configurations or mis-matched protobuf as the typed configurations. This behavioral change can be temporarily reverted by setting runtime guard ``envoy.reloadable_features.no_extension_lookup_by_name`` to false. +```rst +HTTP request header validation is now performed before route selection. This behavioral change can +be temporarily reverted by setting runtime guard +``envoy.reloadable_features.http_validate_headers_before_route_selection`` to ``false``. ``` # PR review policy for maintainers diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index 905305b931c87..44d587659c902 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "8a020cdfb525b78c43df9dea09dc5c99c34712aa21a6a985feb2ec220306924a", + "checksum": "5b2732d6faf4872654fe43d84a7d3424c73c0bbd48835c02915ef6c4443adadc", "crates": { "aho-corasick 1.1.4": { "name": "aho-corasick", @@ -259,6 +259,45 @@ ], "license_file": "LICENSE-APACHE" }, + "autocfg 1.5.0": { + "name": "autocfg", + "version": "1.5.0", + "package_url": "https://github.com/cuviper/autocfg", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/autocfg/1.5.0/download", + "sha256": "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + } + }, + "targets": [ + { + "Library": { + "crate_name": "autocfg", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "autocfg", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "1.5.0" + }, + "license": "Apache-2.0 OR MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "bindgen 0.70.1": { "name": "bindgen", "version": "0.70.1", @@ -667,6 +706,71 @@ ], "license_file": "LICENSE-APACHE" }, + "chacha20 0.10.0": { + "name": "chacha20", + "version": "0.10.0", + "package_url": "https://github.com/RustCrypto/stream-ciphers", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/chacha20/0.10.0/download", + "sha256": "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" + } + }, + "targets": [ + { + "Library": { + "crate_name": "chacha20", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "chacha20", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "rng" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "cfg-if 1.0.4", + "target": "cfg_if" + }, + { + "id": "rand_core 0.10.1", + "target": "rand_core" + } + ], + "selects": { + "cfg(any(target_arch = \"x86_64\", target_arch = \"x86\"))": [ + { + "id": "cpufeatures 0.3.0", + "target": "cpufeatures" + } + ] + } + }, + "edition": "2024", + "version": "0.10.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "clang-sys 1.8.1": { "name": "clang-sys", "version": "1.8.1", @@ -774,20 +878,20 @@ ], "license_file": "LICENSE.txt" }, - "critical-section 1.2.0": { - "name": "critical-section", - "version": "1.2.0", - "package_url": "https://github.com/rust-embedded/critical-section", + "combine 4.6.7": { + "name": "combine", + "version": "4.6.7", + "package_url": "https://github.com/Marwes/combine", "repository": { "Http": { - "url": "https://static.crates.io/crates/critical-section/1.2.0/download", - "sha256": "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + "url": "https://static.crates.io/crates/combine/4.6.7/download", + "sha256": "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" } }, "targets": [ { "Library": { - "crate_name": "critical_section", + "crate_name": "combine", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -798,35 +902,43 @@ } } ], - "library_target_name": "critical_section", + "library_target_name": "combine", "common_attrs": { "compile_data_glob": [ "**" ], + "deps": { + "common": [ + { + "id": "memchr 2.8.0", + "target": "memchr" + } + ], + "selects": {} + }, "edition": "2018", - "version": "1.2.0" + "version": "4.6.7" }, - "license": "MIT OR Apache-2.0", + "license": "MIT", "license_ids": [ - "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": "LICENSE" }, - "crossbeam-channel 0.5.15": { - "name": "crossbeam-channel", - "version": "0.5.15", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "core-foundation 0.9.4": { + "name": "core-foundation", + "version": "0.9.4", + "package_url": "https://github.com/servo/core-foundation-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-channel/0.5.15/download", - "sha256": "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" + "url": "https://static.crates.io/crates/core-foundation/0.9.4/download", + "sha256": "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_channel", + "crate_name": "core_foundation", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -837,7 +949,7 @@ } } ], - "library_target_name": "crossbeam_channel", + "library_target_name": "core_foundation", "common_attrs": { "compile_data_glob": [ "**" @@ -845,21 +957,25 @@ "crate_features": { "common": [ "default", - "std" + "link" ], "selects": {} }, "deps": { "common": [ { - "id": "crossbeam-utils 0.8.21", - "target": "crossbeam_utils" + "id": "core-foundation-sys 0.8.7", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.183", + "target": "libc" } ], "selects": {} }, - "edition": "2021", - "version": "0.5.15" + "edition": "2018", + "version": "0.9.4" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -868,20 +984,20 @@ ], "license_file": "LICENSE-APACHE" }, - "crossbeam-epoch 0.9.18": { - "name": "crossbeam-epoch", - "version": "0.9.18", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "core-foundation-sys 0.8.7": { + "name": "core-foundation-sys", + "version": "0.8.7", + "package_url": "https://github.com/servo/core-foundation-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-epoch/0.9.18/download", - "sha256": "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" + "url": "https://static.crates.io/crates/core-foundation-sys/0.8.7/download", + "sha256": "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_epoch", + "crate_name": "core_foundation_sys", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -892,30 +1008,20 @@ } } ], - "library_target_name": "crossbeam_epoch", + "library_target_name": "core_foundation_sys", "common_attrs": { "compile_data_glob": [ "**" ], "crate_features": { "common": [ - "alloc", "default", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "crossbeam-utils 0.8.21", - "target": "crossbeam_utils" - } + "link" ], "selects": {} }, - "edition": "2021", - "version": "0.9.18" + "edition": "2018", + "version": "0.8.7" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -924,20 +1030,20 @@ ], "license_file": "LICENSE-APACHE" }, - "crossbeam-utils 0.8.21": { - "name": "crossbeam-utils", - "version": "0.8.21", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "cpufeatures 0.3.0": { + "name": "cpufeatures", + "version": "0.3.0", + "package_url": "https://github.com/RustCrypto/utils", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-utils/0.8.21/download", - "sha256": "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + "url": "https://static.crates.io/crates/cpufeatures/0.3.0/download", + "sha256": "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_utils", + "crate_name": "cpufeatures", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -946,54 +1052,44 @@ ] } } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } } ], - "library_target_name": "crossbeam_utils", + "library_target_name": "cpufeatures", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "default", - "std" - ], - "selects": {} - }, "deps": { - "common": [ - { - "id": "crossbeam-utils 0.8.21", - "target": "build_script_build" - } - ], - "selects": {} + "common": [], + "selects": { + "cfg(all(target_arch = \"aarch64\", target_os = \"android\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"aarch64\", target_vendor = \"apple\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"loongarch64\", target_os = \"linux\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ] + } }, - "edition": "2021", - "version": "0.8.21" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] + "edition": "2024", + "version": "0.3.0" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -1002,20 +1098,20 @@ ], "license_file": "LICENSE-APACHE" }, - "data-encoding 2.10.0": { - "name": "data-encoding", - "version": "2.10.0", - "package_url": "https://github.com/ia0/data-encoding", + "critical-section 1.2.0": { + "name": "critical-section", + "version": "1.2.0", + "package_url": "https://github.com/rust-embedded/critical-section", "repository": { "Http": { - "url": "https://static.crates.io/crates/data-encoding/2.10.0/download", - "sha256": "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + "url": "https://static.crates.io/crates/critical-section/1.2.0/download", + "sha256": "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" } }, "targets": [ { "Library": { - "crate_name": "data_encoding", + "crate_name": "critical_section", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -1026,35 +1122,263 @@ } } ], - "library_target_name": "data_encoding", + "library_target_name": "critical_section", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "alloc", - "std" - ], - "selects": {} - }, "edition": "2018", - "version": "2.10.0" + "version": "1.2.0" }, - "license": "MIT", + "license": "MIT OR Apache-2.0", "license_ids": [ + "Apache-2.0", "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, - "deranged 0.5.8": { - "name": "deranged", - "version": "0.5.8", - "package_url": "https://github.com/jhpratt/deranged", + "crossbeam-channel 0.5.15": { + "name": "crossbeam-channel", + "version": "0.5.15", + "package_url": "https://github.com/crossbeam-rs/crossbeam", "repository": { "Http": { - "url": "https://static.crates.io/crates/deranged/0.5.8/download", - "sha256": "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + "url": "https://static.crates.io/crates/crossbeam-channel/0.5.15/download", + "sha256": "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_channel", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_channel", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "crossbeam_utils" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.5.15" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "crossbeam-epoch 0.9.18": { + "name": "crossbeam-epoch", + "version": "0.9.18", + "package_url": "https://github.com/crossbeam-rs/crossbeam", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/crossbeam-epoch/0.9.18/download", + "sha256": "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_epoch", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_epoch", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "alloc", + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "crossbeam_utils" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.9.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "crossbeam-utils 0.8.21": { + "name": "crossbeam-utils", + "version": "0.8.21", + "package_url": "https://github.com/crossbeam-rs/crossbeam", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/crossbeam-utils/0.8.21/download", + "sha256": "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_utils", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_utils", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.8.21" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ] + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "data-encoding 2.10.0": { + "name": "data-encoding", + "version": "2.10.0", + "package_url": "https://github.com/ia0/data-encoding", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/data-encoding/2.10.0/download", + "sha256": "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + } + }, + "targets": [ + { + "Library": { + "crate_name": "data_encoding", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "data_encoding", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "alloc", + "std" + ], + "selects": {} + }, + "edition": "2018", + "version": "2.10.0" + }, + "license": "MIT", + "license_ids": [ + "MIT" + ], + "license_file": "LICENSE" + }, + "deranged 0.5.8": { + "name": "deranged", + "version": "0.5.8", + "package_url": "https://github.com/jhpratt/deranged", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/deranged/0.5.8/download", + "sha256": "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" } }, "targets": [ @@ -1232,70 +1556,17 @@ "compile_data_glob": [ "**" ], - "edition": "2021", - "version": "1.15.0" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "enum-as-inner 0.6.1": { - "name": "enum-as-inner", - "version": "0.6.1", - "package_url": "https://github.com/bluejekyll/enum-as-inner", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/enum-as-inner/0.6.1/download", - "sha256": "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" - } - }, - "targets": [ - { - "ProcMacro": { - "crate_name": "enum_as_inner", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "enum_as_inner", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { + "crate_features": { "common": [ - { - "id": "heck 0.5.0", - "target": "heck" - }, - { - "id": "proc-macro2 1.0.106", - "target": "proc_macro2" - }, - { - "id": "quote 1.0.45", - "target": "quote" - }, - { - "id": "syn 2.0.117", - "target": "syn" - } + "default", + "std" ], "selects": {} }, - "edition": "2018", - "version": "0.6.1" + "edition": "2021", + "version": "1.15.0" }, - "license": "MIT/Apache-2.0", + "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" @@ -1316,7 +1587,7 @@ "deps": { "common": [ { - "id": "hickory-resolver 0.25.2", + "id": "hickory-resolver 0.26.1", "target": "hickory_resolver" }, { @@ -1831,6 +2102,62 @@ ], "license_file": "LICENSE-APACHE" }, + "futures-macro 0.3.32": { + "name": "futures-macro", + "version": "0.3.32", + "package_url": "https://github.com/rust-lang/futures-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/futures-macro/0.3.32/download", + "sha256": "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "futures_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "futures_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "proc-macro2 1.0.106", + "target": "proc_macro2" + }, + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.3.32" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "futures-sink 0.3.32": { "name": "futures-sink", "version": "0.3.32", @@ -1956,6 +2283,9 @@ "crate_features": { "common": [ "alloc", + "async-await", + "async-await-macro", + "futures-macro", "slab", "std" ], @@ -1977,74 +2307,22 @@ }, { "id": "slab 0.4.12", - "target": "slab" - } - ], - "selects": {} - }, - "edition": "2018", - "version": "0.3.32" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "getrandom 0.2.17": { - "name": "getrandom", - "version": "0.2.17", - "package_url": "https://github.com/rust-random/getrandom", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/getrandom/0.2.17/download", - "sha256": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" - } - }, - "targets": [ - { - "Library": { - "crate_name": "getrandom", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "getrandom", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { + "target": "slab" + } + ], + "selects": {} + }, + "edition": "2018", + "proc_macro_deps": { "common": [ { - "id": "cfg-if 1.0.4", - "target": "cfg_if" + "id": "futures-macro 0.3.32", + "target": "futures_macro" } ], - "selects": { - "cfg(target_os = \"wasi\")": [ - { - "id": "wasi 0.11.1+wasi-snapshot-preview1", - "target": "wasi" - } - ], - "cfg(unix)": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ] - } + "selects": {} }, - "edition": "2018", - "version": "0.2.17" + "version": "0.3.32" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2053,14 +2331,14 @@ ], "license_file": "LICENSE-APACHE" }, - "getrandom 0.3.4": { + "getrandom 0.2.17": { "name": "getrandom", - "version": "0.3.4", + "version": "0.2.17", "package_url": "https://github.com/rust-random/getrandom", "repository": { "Http": { - "url": "https://static.crates.io/crates/getrandom/0.3.4/download", - "sha256": "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" + "url": "https://static.crates.io/crates/getrandom/0.2.17/download", + "sha256": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" } }, "targets": [ @@ -2075,18 +2353,6 @@ ] } } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } } ], "library_target_name": "getrandom", @@ -2094,79 +2360,21 @@ "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "std" - ], - "selects": {} - }, "deps": { "common": [ { "id": "cfg-if 1.0.4", "target": "cfg_if" - }, - { - "id": "getrandom 0.3.4", - "target": "build_script_build" } ], "selects": { - "cfg(all(any(target_os = \"linux\", target_os = \"android\"), not(any(all(target_os = \"linux\", target_env = \"\"), getrandom_backend = \"custom\", getrandom_backend = \"linux_raw\", getrandom_backend = \"rdrand\", getrandom_backend = \"rndr\"))))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p2\"))": [ - { - "id": "wasip2 1.0.2+wasi-0.2.9", - "target": "wasip2" - } - ], - "cfg(all(target_os = \"uefi\", getrandom_backend = \"efi_rng\"))": [ - { - "id": "r-efi 5.3.0", - "target": "r_efi" - } - ], - "cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"hurd\", target_os = \"illumos\", target_os = \"cygwin\", all(target_os = \"horizon\", target_arch = \"arm\")))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"haiku\", target_os = \"redox\", target_os = \"nto\", target_os = \"aix\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"ios\", target_os = \"visionos\", target_os = \"watchos\", target_os = \"tvos\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"macos\", target_os = \"openbsd\", target_os = \"vita\", target_os = \"emscripten\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(target_os = \"netbsd\")": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(target_os = \"solaris\")": [ + "cfg(target_os = \"wasi\")": [ { - "id": "libc 0.2.183", - "target": "libc" + "id": "wasi 0.11.1+wasi-snapshot-preview1", + "target": "wasi" } ], - "cfg(target_os = \"vxworks\")": [ + "cfg(unix)": [ { "id": "libc 0.2.183", "target": "libc" @@ -2174,19 +2382,8 @@ ] } }, - "edition": "2021", - "version": "0.3.4" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] + "edition": "2018", + "version": "0.2.17" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2236,6 +2433,13 @@ "compile_data_glob": [ "**" ], + "crate_features": { + "common": [ + "std", + "sys_rng" + ], + "selects": {} + }, "deps": { "common": [ { @@ -2245,6 +2449,10 @@ { "id": "getrandom 0.4.2", "target": "build_script_build" + }, + { + "id": "rand_core 0.10.1", + "target": "rand_core" } ], "selects": { @@ -2601,20 +2809,20 @@ ], "license_file": "LICENSE-APACHE" }, - "hickory-proto 0.25.2": { - "name": "hickory-proto", - "version": "0.25.2", + "hickory-net 0.26.1": { + "name": "hickory-net", + "version": "0.26.1", "package_url": "https://github.com/hickory-dns/hickory-dns", "repository": { "Http": { - "url": "https://static.crates.io/crates/hickory-proto/0.25.2/download", - "sha256": "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" + "url": "https://static.crates.io/crates/hickory-net/0.26.1/download", + "sha256": "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" } }, "targets": [ { "Library": { - "crate_name": "hickory_proto", + "crate_name": "hickory_net", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -2625,7 +2833,7 @@ } } ], - "library_target_name": "hickory_proto", + "library_target_name": "hickory_net", "common_attrs": { "compile_data_glob": [ "**" @@ -2636,9 +2844,7 @@ "__https", "__tls", "dnssec-ring", - "futures-io", "https-ring", - "std", "tls-ring", "tokio", "webpki-roots" @@ -2679,6 +2885,10 @@ "id": "h2 0.4.13", "target": "h2" }, + { + "id": "hickory-proto 0.26.1", + "target": "hickory_proto" + }, { "id": "http 1.4.0", "target": "http" @@ -2692,11 +2902,15 @@ "target": "ipnet" }, { - "id": "once_cell 1.21.4", - "target": "once_cell" + "id": "lru-cache 0.1.2", + "target": "lru_cache" + }, + { + "id": "parking_lot 0.12.5", + "target": "parking_lot" }, { - "id": "rand 0.9.2", + "id": "rand 0.10.1", "target": "rand" }, { @@ -2740,11 +2954,18 @@ "target": "url" }, { - "id": "webpki-roots 0.26.11", + "id": "webpki-roots 1.0.6", "target": "webpki_roots" } ], - "selects": {} + "selects": { + "cfg(target_os = \"android\")": [ + { + "id": "jni 0.22.4", + "target": "jni" + } + ] + } }, "edition": "2021", "proc_macro_deps": { @@ -2752,15 +2973,127 @@ { "id": "async-trait 0.1.89", "target": "async_trait" + } + ], + "selects": {} + }, + "version": "0.26.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "hickory-proto 0.26.1": { + "name": "hickory-proto", + "version": "0.26.1", + "package_url": "https://github.com/hickory-dns/hickory-dns", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/hickory-proto/0.26.1/download", + "sha256": "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" + } + }, + "targets": [ + { + "Library": { + "crate_name": "hickory_proto", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "hickory_proto", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "__dnssec", + "access-control", + "dnssec-ring", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "bitflags 2.11.0", + "target": "bitflags" + }, + { + "id": "data-encoding 2.10.0", + "target": "data_encoding" + }, + { + "id": "idna 1.1.0", + "target": "idna" + }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, + { + "id": "once_cell 1.21.4", + "target": "once_cell" + }, + { + "id": "prefix-trie 0.8.3", + "target": "prefix_trie" + }, + { + "id": "rand 0.10.1", + "target": "rand" + }, + { + "id": "ring 0.17.14", + "target": "ring" + }, + { + "id": "rustls-pki-types 1.14.0", + "target": "rustls_pki_types" + }, + { + "id": "thiserror 2.0.18", + "target": "thiserror" + }, + { + "id": "time 0.3.47", + "target": "time" + }, + { + "id": "tinyvec 1.11.0", + "target": "tinyvec" + }, + { + "id": "tracing 0.1.44", + "target": "tracing" }, { - "id": "enum-as-inner 0.6.1", - "target": "enum_as_inner" + "id": "url 2.5.8", + "target": "url" } ], - "selects": {} + "selects": { + "cfg(target_os = \"android\")": [ + { + "id": "jni 0.22.4", + "target": "jni" + } + ] + } }, - "version": "0.25.2" + "edition": "2021", + "version": "0.26.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2769,14 +3102,14 @@ ], "license_file": "LICENSE-APACHE" }, - "hickory-resolver 0.25.2": { + "hickory-resolver 0.26.1": { "name": "hickory-resolver", - "version": "0.25.2", + "version": "0.26.1", "package_url": "https://github.com/hickory-dns/hickory-dns", "repository": { "Http": { - "url": "https://static.crates.io/crates/hickory-resolver/0.25.2/download", - "sha256": "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" + "url": "https://static.crates.io/crates/hickory-resolver/0.26.1/download", + "sha256": "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" } }, "targets": [ @@ -2824,9 +3157,17 @@ "target": "futures_util" }, { - "id": "hickory-proto 0.25.2", + "id": "hickory-net 0.26.1", + "target": "hickory_net" + }, + { + "id": "hickory-proto 0.26.1", "target": "hickory_proto" }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, { "id": "moka 0.12.15", "target": "moka" @@ -2840,7 +3181,7 @@ "target": "parking_lot" }, { - "id": "rand 0.9.2", + "id": "rand 0.10.1", "target": "rand" }, { @@ -2872,11 +3213,17 @@ "target": "tracing" }, { - "id": "webpki-roots 0.26.11", + "id": "webpki-roots 1.0.6", "target": "webpki_roots" } ], "selects": { + "aarch64-apple-darwin": [ + { + "id": "system-configuration 0.7.0", + "target": "system_configuration" + } + ], "x86_64-pc-windows-msvc": [ { "id": "ipconfig 0.3.4", @@ -2886,7 +3233,7 @@ } }, "edition": "2021", - "version": "0.25.2" + "version": "0.26.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -3859,41 +4206,355 @@ } } ], - "library_target_name": "ipnet", + "library_target_name": "ipnet", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "serde", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "serde 1.0.228", + "target": "serde" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "2.12.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "itertools 0.13.0": { + "name": "itertools", + "version": "0.13.0", + "package_url": "https://github.com/rust-itertools/itertools", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/itertools/0.13.0/download", + "sha256": "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" + } + }, + "targets": [ + { + "Library": { + "crate_name": "itertools", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "itertools", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "either 1.15.0", + "target": "either" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.13.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "itoa 1.0.18": { + "name": "itoa", + "version": "1.0.18", + "package_url": "https://github.com/dtolnay/itoa", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/itoa/1.0.18/download", + "sha256": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + } + }, + "targets": [ + { + "Library": { + "crate_name": "itoa", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "itoa", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2021", + "version": "1.0.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "jni 0.22.4": { + "name": "jni", + "version": "0.22.4", + "package_url": "https://github.com/jni-rs/jni-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/jni/0.22.4/download", + "sha256": "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" + } + }, + "targets": [ + { + "Library": { + "crate_name": "jni", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "jni", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "cfg-if 1.0.4", + "target": "cfg_if" + }, + { + "id": "combine 4.6.7", + "target": "combine" + }, + { + "id": "jni 0.22.4", + "target": "build_script_build" + }, + { + "id": "jni-sys 0.4.1", + "target": "jni_sys" + }, + { + "id": "log 0.4.29", + "target": "log" + }, + { + "id": "simd_cesu8 1.1.1", + "target": "simd_cesu8" + }, + { + "id": "thiserror 2.0.18", + "target": "thiserror" + } + ], + "selects": { + "cfg(windows)": [ + { + "id": "windows-link 0.2.1", + "target": "windows_link" + } + ] + } + }, + "edition": "2024", + "proc_macro_deps": { + "common": [ + { + "id": "jni-macros 0.22.4", + "target": "jni_macros" + } + ], + "selects": {} + }, + "version": "0.22.4" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "walkdir 2.5.0", + "target": "walkdir" + } + ], + "selects": {} + } + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": null + }, + "jni-macros 0.22.4": { + "name": "jni-macros", + "version": "0.22.4", + "package_url": "https://github.com/jni-rs/jni-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/jni-macros/0.22.4/download", + "sha256": "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "jni_macros", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "jni_macros", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { + "deps": { "common": [ - "std" + { + "id": "jni-macros 0.22.4", + "target": "build_script_build" + }, + { + "id": "proc-macro2 1.0.106", + "target": "proc_macro2" + }, + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "simd_cesu8 1.1.1", + "target": "simd_cesu8" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } ], "selects": {} }, - "edition": "2018", - "version": "2.12.0" + "edition": "2024", + "version": "0.22.4" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "rustc_version 0.4.1", + "target": "rustc_version" + } + ], + "selects": {} + } }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": null }, - "itertools 0.13.0": { - "name": "itertools", - "version": "0.13.0", - "package_url": "https://github.com/rust-itertools/itertools", + "jni-sys 0.4.1": { + "name": "jni-sys", + "version": "0.4.1", + "package_url": "https://github.com/jni-rs/jni-sys", "repository": { "Http": { - "url": "https://static.crates.io/crates/itertools/0.13.0/download", - "sha256": "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" + "url": "https://static.crates.io/crates/jni-sys/0.4.1/download", + "sha256": "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" } }, "targets": [ { "Library": { - "crate_name": "itertools", + "crate_name": "jni_sys", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -3904,22 +4565,22 @@ } } ], - "library_target_name": "itertools", + "library_target_name": "jni_sys", "common_attrs": { "compile_data_glob": [ "**" ], - "deps": { + "edition": "2021", + "proc_macro_deps": { "common": [ { - "id": "either 1.15.0", - "target": "either" + "id": "jni-sys-macros 0.4.1", + "target": "jni_sys_macros" } ], "selects": {} }, - "edition": "2018", - "version": "0.13.0" + "version": "0.4.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -3928,20 +4589,20 @@ ], "license_file": "LICENSE-APACHE" }, - "itoa 1.0.18": { - "name": "itoa", - "version": "1.0.18", - "package_url": "https://github.com/dtolnay/itoa", + "jni-sys-macros 0.4.1": { + "name": "jni-sys-macros", + "version": "0.4.1", + "package_url": "https://github.com/jni-rs/jni-sys", "repository": { "Http": { - "url": "https://static.crates.io/crates/itoa/1.0.18/download", - "sha256": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + "url": "https://static.crates.io/crates/jni-sys-macros/0.4.1/download", + "sha256": "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" } }, "targets": [ { - "Library": { - "crate_name": "itoa", + "ProcMacro": { + "crate_name": "jni_sys_macros", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -3952,20 +4613,33 @@ } } ], - "library_target_name": "itoa", + "library_target_name": "jni_sys_macros", "common_attrs": { "compile_data_glob": [ "**" ], + "deps": { + "common": [ + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } + ], + "selects": {} + }, "edition": "2021", - "version": "1.0.18" + "version": "0.4.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": null }, "js-sys 0.3.91": { "name": "js-sys", @@ -4209,6 +4883,45 @@ ], "license_file": "LICENSE" }, + "linked-hash-map 0.5.6": { + "name": "linked-hash-map", + "version": "0.5.6", + "package_url": "https://github.com/contain-rs/linked-hash-map", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/linked-hash-map/0.5.6/download", + "sha256": "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + } + }, + "targets": [ + { + "Library": { + "crate_name": "linked_hash_map", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "linked_hash_map", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "0.5.6" + }, + "license": "MIT/Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "litemap 0.8.1": { "name": "litemap", "version": "0.8.1", @@ -4341,6 +5054,54 @@ ], "license_file": "LICENSE-APACHE" }, + "lru-cache 0.1.2": { + "name": "lru-cache", + "version": "0.1.2", + "package_url": "https://github.com/contain-rs/lru-cache", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/lru-cache/0.1.2/download", + "sha256": "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" + } + }, + "targets": [ + { + "Library": { + "crate_name": "lru_cache", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "lru_cache", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "linked-hash-map 0.5.6", + "target": "linked_hash_map" + } + ], + "selects": {} + }, + "edition": "2015", + "version": "0.1.2" + }, + "license": "MIT/Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "memchr 2.8.0": { "name": "memchr", "version": "2.8.0", @@ -4758,6 +5519,45 @@ ], "license_file": "LICENSE-APACHE" }, + "ndk-context 0.1.1": { + "name": "ndk-context", + "version": "0.1.1", + "package_url": "https://github.com/rust-windowing/android-ndk-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/ndk-context/0.1.1/download", + "sha256": "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "ndk_context", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "ndk_context", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2021", + "version": "0.1.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": null + }, "nom 7.1.3": { "name": "nom", "version": "7.1.3", @@ -4845,15 +5645,102 @@ "compile_data_glob": [ "**" ], - "edition": "2021", - "version": "0.2.1" + "edition": "2021", + "version": "0.2.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-Apache" + }, + "num-traits 0.2.19": { + "name": "num-traits", + "version": "0.2.19", + "package_url": "https://github.com/rust-num/num-traits", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/num-traits/0.2.19/download", + "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" + } + }, + "targets": [ + { + "Library": { + "crate_name": "num_traits", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "num_traits", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "num-traits 0.2.19", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.2.19" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "autocfg 1.5.0", + "target": "autocfg" + } + ], + "selects": {} + } }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-Apache" + "license_file": "LICENSE-APACHE" }, "once_cell 1.21.4": { "name": "once_cell", @@ -5329,61 +6216,6 @@ ], "license_file": "LICENSE-Apache" }, - "ppv-lite86 0.2.21": { - "name": "ppv-lite86", - "version": "0.2.21", - "package_url": "https://github.com/cryptocorrosion/cryptocorrosion", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/ppv-lite86/0.2.21/download", - "sha256": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" - } - }, - "targets": [ - { - "Library": { - "crate_name": "ppv_lite86", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "ppv_lite86", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "simd", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "zerocopy 0.8.47", - "target": "zerocopy" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.2.21" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, "predicates 3.1.4": { "name": "predicates", "version": "3.1.4", @@ -5527,6 +6359,69 @@ ], "license_file": "LICENSE-APACHE" }, + "prefix-trie 0.8.3": { + "name": "prefix-trie", + "version": "0.8.3", + "package_url": "https://github.com/tiborschneider/prefix-trie", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/prefix-trie/0.8.3/download", + "sha256": "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" + } + }, + "targets": [ + { + "Library": { + "crate_name": "prefix_trie", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "prefix_trie", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "ipnet" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "either 1.15.0", + "target": "either" + }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, + { + "id": "num-traits 0.2.19", + "target": "num_traits" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.8.3" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "prettyplease 0.2.37": { "name": "prettyplease", "version": "0.2.37", @@ -5777,46 +6672,6 @@ ], "license_file": "LICENSE-APACHE" }, - "r-efi 5.3.0": { - "name": "r-efi", - "version": "5.3.0", - "package_url": "https://github.com/r-efi/r-efi", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/r-efi/5.3.0/download", - "sha256": "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - } - }, - "targets": [ - { - "Library": { - "crate_name": "r_efi", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "r_efi", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "edition": "2018", - "version": "5.3.0" - }, - "license": "MIT OR Apache-2.0 OR LGPL-2.1-or-later", - "license_ids": [ - "Apache-2.0", - "LGPL-2.1", - "MIT" - ], - "license_file": null - }, "r-efi 6.0.0": { "name": "r-efi", "version": "6.0.0", @@ -5857,14 +6712,14 @@ ], "license_file": null }, - "rand 0.9.2": { + "rand 0.10.1": { "name": "rand", - "version": "0.9.2", + "version": "0.10.1", "package_url": "https://github.com/rust-random/rand", "repository": { "Http": { - "url": "https://static.crates.io/crates/rand/0.9.2/download", - "sha256": "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" + "url": "https://static.crates.io/crates/rand/0.10.1/download", + "sha256": "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" } }, "targets": [ @@ -5889,9 +6744,9 @@ "crate_features": { "common": [ "alloc", - "os_rng", "std", "std_rng", + "sys_rng", "thread_rng" ], "selects": {} @@ -5899,76 +6754,22 @@ "deps": { "common": [ { - "id": "rand_chacha 0.9.0", - "target": "rand_chacha" + "id": "chacha20 0.10.0", + "target": "chacha20" }, { - "id": "rand_core 0.9.5", - "target": "rand_core" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.9.2" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "rand_chacha 0.9.0": { - "name": "rand_chacha", - "version": "0.9.0", - "package_url": "https://github.com/rust-random/rand", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/rand_chacha/0.9.0/download", - "sha256": "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" - } - }, - "targets": [ - { - "Library": { - "crate_name": "rand_chacha", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "rand_chacha", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "ppv-lite86 0.2.21", - "target": "ppv_lite86" + "id": "getrandom 0.4.2", + "target": "getrandom" }, { - "id": "rand_core 0.9.5", + "id": "rand_core 0.10.1", "target": "rand_core" } ], "selects": {} }, - "edition": "2021", - "version": "0.9.0" + "edition": "2024", + "version": "0.10.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -5977,14 +6778,14 @@ ], "license_file": "LICENSE-APACHE" }, - "rand_core 0.9.5": { + "rand_core 0.10.1": { "name": "rand_core", - "version": "0.9.5", - "package_url": "https://github.com/rust-random/rand", + "version": "0.10.1", + "package_url": "https://github.com/rust-random/rand_core", "repository": { "Http": { - "url": "https://static.crates.io/crates/rand_core/0.9.5/download", - "sha256": "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + "url": "https://static.crates.io/crates/rand_core/0.10.1/download", + "sha256": "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" } }, "targets": [ @@ -6006,24 +6807,8 @@ "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "os_rng", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "getrandom 0.3.4", - "target": "getrandom" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.9.5" + "edition": "2024", + "version": "0.10.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -6417,14 +7202,60 @@ "package_url": "https://github.com/rust-lang-nursery/rustc-hash", "repository": { "Http": { - "url": "https://static.crates.io/crates/rustc-hash/1.1.0/download", - "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + "url": "https://static.crates.io/crates/rustc-hash/1.1.0/download", + "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + } + }, + "targets": [ + { + "Library": { + "crate_name": "rustc_hash", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "rustc_hash", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "edition": "2015", + "version": "1.1.0" + }, + "license": "Apache-2.0/MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "rustc_version 0.4.1": { + "name": "rustc_version", + "version": "0.4.1", + "package_url": "https://github.com/djc/rustc-version-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/rustc_version/0.4.1/download", + "sha256": "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" } }, "targets": [ { "Library": { - "crate_name": "rustc_hash", + "crate_name": "rustc_version", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -6435,22 +7266,24 @@ } } ], - "library_target_name": "rustc_hash", + "library_target_name": "rustc_version", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { + "deps": { "common": [ - "default", - "std" + { + "id": "semver 1.0.27", + "target": "semver" + } ], "selects": {} }, - "edition": "2015", - "version": "1.1.0" + "edition": "2018", + "version": "0.4.1" }, - "license": "Apache-2.0/MIT", + "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" @@ -6532,7 +7365,7 @@ "alias": "pki_types" }, { - "id": "rustls-webpki 0.103.10", + "id": "rustls-webpki 0.103.13", "target": "webpki" }, { @@ -6633,14 +7466,14 @@ ], "license_file": "LICENSE-APACHE" }, - "rustls-webpki 0.103.10": { + "rustls-webpki 0.103.13": { "name": "rustls-webpki", - "version": "0.103.10", + "version": "0.103.13", "package_url": "https://github.com/rustls/webpki", "repository": { "Http": { - "url": "https://static.crates.io/crates/rustls-webpki/0.103.10/download", - "sha256": "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" + "url": "https://static.crates.io/crates/rustls-webpki/0.103.13/download", + "sha256": "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" } }, "targets": [ @@ -6689,7 +7522,7 @@ "selects": {} }, "edition": "2021", - "version": "0.103.10" + "version": "0.103.13" }, "license": "ISC", "license_ids": [ @@ -6768,6 +7601,56 @@ ], "license_file": "LICENSE-APACHE" }, + "same-file 1.0.6": { + "name": "same-file", + "version": "1.0.6", + "package_url": "https://github.com/BurntSushi/same-file", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/same-file/1.0.6/download", + "sha256": "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" + } + }, + "targets": [ + { + "Library": { + "crate_name": "same_file", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "same_file", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [], + "selects": { + "cfg(windows)": [ + { + "id": "winapi-util 0.1.11", + "target": "winapi_util" + } + ] + } + }, + "edition": "2018", + "version": "1.0.6" + }, + "license": "Unlicense/MIT", + "license_ids": [ + "MIT", + "Unlicense" + ], + "license_file": "LICENSE-MIT" + }, "scopeguard 1.2.0": { "name": "scopeguard", "version": "1.2.0", @@ -6836,6 +7719,13 @@ "compile_data_glob": [ "**" ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, "edition": "2018", "version": "1.0.27" }, @@ -7269,6 +8159,106 @@ ], "license_file": "LICENSE-APACHE" }, + "simd_cesu8 1.1.1": { + "name": "simd_cesu8", + "version": "1.1.1", + "package_url": "https://github.com/seancroach/simd_cesu8", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/simd_cesu8/1.1.1/download", + "sha256": "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" + } + }, + "targets": [ + { + "Library": { + "crate_name": "simd_cesu8", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "simd_cesu8", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "simdutf8 0.1.5", + "target": "simdutf8" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "1.1.1" + }, + "license": "Apache-2.0 OR MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "simdutf8 0.1.5": { + "name": "simdutf8", + "version": "0.1.5", + "package_url": "https://github.com/rusticstuff/simdutf8", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/simdutf8/0.1.5/download", + "sha256": "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + } + }, + "targets": [ + { + "Library": { + "crate_name": "simdutf8", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "simdutf8", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "std" + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.5" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-Apache" + }, "slab 0.4.12": { "name": "slab", "version": "0.4.12", @@ -7626,11 +8616,146 @@ "edition": "2018", "version": "0.13.2" }, - "license": "MIT", + "license": "MIT", + "license_ids": [ + "MIT" + ], + "license_file": "LICENSE" + }, + "system-configuration 0.7.0": { + "name": "system-configuration", + "version": "0.7.0", + "package_url": "https://github.com/mullvad/system-configuration-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/system-configuration/0.7.0/download", + "sha256": "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "system_configuration", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "system_configuration", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "bitflags 2.11.0", + "target": "bitflags" + }, + { + "id": "core-foundation 0.9.4", + "target": "core_foundation" + }, + { + "id": "system-configuration-sys 0.6.0", + "target": "system_configuration_sys" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.7.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "system-configuration-sys 0.6.0": { + "name": "system-configuration-sys", + "version": "0.6.0", + "package_url": "https://github.com/mullvad/system-configuration-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/system-configuration-sys/0.6.0/download", + "sha256": "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" + } + }, + "targets": [ + { + "Library": { + "crate_name": "system_configuration_sys", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "system_configuration_sys", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "core-foundation-sys 0.8.7", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.183", + "target": "libc" + }, + { + "id": "system-configuration-sys 0.6.0", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.6.0" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ] + }, + "license": "MIT OR Apache-2.0", "license_ids": [ + "Apache-2.0", "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, "tagptr 0.2.0": { "name": "tagptr", @@ -8884,6 +10009,61 @@ ], "license_file": "LICENSE-APACHE" }, + "walkdir 2.5.0": { + "name": "walkdir", + "version": "2.5.0", + "package_url": "https://github.com/BurntSushi/walkdir", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/walkdir/2.5.0/download", + "sha256": "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "walkdir", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "walkdir", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "same-file 1.0.6", + "target": "same_file" + } + ], + "selects": { + "cfg(windows)": [ + { + "id": "winapi-util 0.1.11", + "target": "winapi_util" + } + ] + } + }, + "edition": "2018", + "version": "2.5.0" + }, + "license": "Unlicense/MIT", + "license_ids": [ + "MIT", + "Unlicense" + ], + "license_file": "LICENSE-MIT" + }, "wasi 0.11.1+wasi-snapshot-preview1": { "name": "wasi", "version": "0.11.1+wasi-snapshot-preview1", @@ -9520,14 +10700,14 @@ ], "license_file": null }, - "webpki-roots 0.26.11": { + "webpki-roots 1.0.6": { "name": "webpki-roots", - "version": "0.26.11", + "version": "1.0.6", "package_url": "https://github.com/rustls/webpki-roots", "repository": { "Http": { - "url": "https://static.crates.io/crates/webpki-roots/0.26.11/download", - "sha256": "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" + "url": "https://static.crates.io/crates/webpki-roots/1.0.6/download", + "sha256": "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" } }, "targets": [ @@ -9552,15 +10732,15 @@ "deps": { "common": [ { - "id": "webpki-roots 1.0.6", - "target": "webpki_roots", - "alias": "parent" + "id": "rustls-pki-types 1.14.0", + "target": "rustls_pki_types", + "alias": "pki_types" } ], "selects": {} }, "edition": "2021", - "version": "0.26.11" + "version": "1.0.6" }, "license": "CDLA-Permissive-2.0", "license_ids": [ @@ -9568,20 +10748,20 @@ ], "license_file": "LICENSE" }, - "webpki-roots 1.0.6": { - "name": "webpki-roots", - "version": "1.0.6", - "package_url": "https://github.com/rustls/webpki-roots", + "widestring 1.2.1": { + "name": "widestring", + "version": "1.2.1", + "package_url": "https://github.com/VoidStarKat/widestring-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/webpki-roots/1.0.6/download", - "sha256": "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" + "url": "https://static.crates.io/crates/widestring/1.2.1/download", + "sha256": "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" } }, "targets": [ { "Library": { - "crate_name": "webpki_roots", + "crate_name": "widestring", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -9592,44 +10772,43 @@ } } ], - "library_target_name": "webpki_roots", + "library_target_name": "widestring", "common_attrs": { "compile_data_glob": [ "**" ], - "deps": { + "crate_features": { "common": [ - { - "id": "rustls-pki-types 1.14.0", - "target": "rustls_pki_types", - "alias": "pki_types" - } + "alloc", + "default", + "std" ], "selects": {} }, "edition": "2021", - "version": "1.0.6" + "version": "1.2.1" }, - "license": "CDLA-Permissive-2.0", + "license": "MIT OR Apache-2.0", "license_ids": [ - "CDLA-Permissive-2.0" + "Apache-2.0", + "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, - "widestring 1.2.1": { - "name": "widestring", - "version": "1.2.1", - "package_url": "https://github.com/VoidStarKat/widestring-rs", + "winapi-util 0.1.11": { + "name": "winapi-util", + "version": "0.1.11", + "package_url": "https://github.com/BurntSushi/winapi-util", "repository": { "Http": { - "url": "https://static.crates.io/crates/widestring/1.2.1/download", - "sha256": "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + "url": "https://static.crates.io/crates/winapi-util/0.1.11/download", + "sha256": "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" } }, "targets": [ { "Library": { - "crate_name": "widestring", + "crate_name": "winapi_util", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -9640,28 +10819,31 @@ } } ], - "library_target_name": "widestring", + "library_target_name": "winapi_util", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "alloc", - "default", - "std" - ], - "selects": {} + "deps": { + "common": [], + "selects": { + "cfg(windows)": [ + { + "id": "windows-sys 0.61.2", + "target": "windows_sys" + } + ] + } }, "edition": "2021", - "version": "1.2.1" + "version": "0.1.11" }, - "license": "MIT OR Apache-2.0", + "license": "Unlicense OR MIT", "license_ids": [ - "Apache-2.0", - "MIT" + "MIT", + "Unlicense" ], - "license_file": "LICENSE-APACHE" + "license_file": "LICENSE-MIT" }, "windows-link 0.2.1": { "name": "windows-link", @@ -11358,152 +12540,6 @@ ], "license_file": "LICENSE" }, - "zerocopy 0.8.47": { - "name": "zerocopy", - "version": "0.8.47", - "package_url": "https://github.com/google/zerocopy", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/zerocopy/0.8.47/download", - "sha256": "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" - } - }, - "targets": [ - { - "Library": { - "crate_name": "zerocopy", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "zerocopy", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "simd" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "zerocopy 0.8.47", - "target": "build_script_build" - } - ], - "selects": {} - }, - "edition": "2021", - "proc_macro_deps": { - "common": [], - "selects": { - "cfg(any())": [ - { - "id": "zerocopy-derive 0.8.47", - "target": "zerocopy_derive" - } - ] - } - }, - "version": "0.8.47" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] - }, - "license": "BSD-2-Clause OR Apache-2.0 OR MIT", - "license_ids": [ - "Apache-2.0", - "BSD-2-Clause", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "zerocopy-derive 0.8.47": { - "name": "zerocopy-derive", - "version": "0.8.47", - "package_url": "https://github.com/google/zerocopy", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/zerocopy-derive/0.8.47/download", - "sha256": "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" - } - }, - "targets": [ - { - "ProcMacro": { - "crate_name": "zerocopy_derive", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "zerocopy_derive", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { - "common": [ - { - "id": "proc-macro2 1.0.106", - "target": "proc_macro2" - }, - { - "id": "quote 1.0.45", - "target": "quote" - }, - { - "id": "syn 2.0.117", - "target": "syn" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.8.47" - }, - "license": "BSD-2-Clause OR Apache-2.0 OR MIT", - "license_ids": [ - "Apache-2.0", - "BSD-2-Clause", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, "zerofrom 0.1.6": { "name": "zerofrom", "version": "0.1.6", @@ -11952,6 +12988,14 @@ "x86_64-unknown-nixos-gnu" ], "cfg(all(target_arch = \"aarch64\", target_env = \"msvc\", not(windows_raw_dylib)))": [], + "cfg(all(target_arch = \"aarch64\", target_os = \"android\"))": [], + "cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))": [ + "aarch64-unknown-linux-gnu" + ], + "cfg(all(target_arch = \"aarch64\", target_vendor = \"apple\"))": [ + "aarch64-apple-darwin" + ], + "cfg(all(target_arch = \"loongarch64\", target_os = \"linux\"))": [], "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p2\"))": [], "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p3\"))": [], "cfg(all(target_arch = \"x86\", target_env = \"gnu\", not(target_abi = \"llvm\"), not(windows_raw_dylib)))": [], @@ -11962,6 +13006,11 @@ ], "cfg(all(target_os = \"uefi\", getrandom_backend = \"efi_rng\"))": [], "cfg(any())": [], + "cfg(any(target_arch = \"x86_64\", target_arch = \"x86\"))": [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-nixos-gnu" + ], "cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"hurd\", target_os = \"illumos\", target_os = \"cygwin\", all(target_os = \"horizon\", target_arch = \"arm\")))": [], "cfg(any(target_os = \"haiku\", target_os = \"redox\", target_os = \"nto\", target_os = \"aix\"))": [], "cfg(any(target_os = \"ios\", target_os = \"visionos\", target_os = \"watchos\", target_os = \"tvos\"))": [], @@ -11975,6 +13024,7 @@ "x86_64-unknown-linux-gnu", "x86_64-unknown-nixos-gnu" ], + "cfg(target_os = \"android\")": [], "cfg(target_os = \"hermit\")": [], "cfg(target_os = \"netbsd\")": [], "cfg(target_os = \"redox\")": [], @@ -12013,7 +13063,7 @@ }, "direct_deps": [ "bindgen 0.70.1", - "hickory-resolver 0.25.2", + "hickory-resolver 0.26.1", "mockall 0.13.1", "serde 1.0.228", "serde_json 1.0.149", diff --git a/Cargo.lock b/Cargo.lock index 755a11ae9dc24..2aec1fc9de04b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bindgen" version = "0.70.1" @@ -103,6 +109,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -114,6 +131,41 @@ dependencies = [ "libloading", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -182,18 +234,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "envoy-dynamic-modules-builtin-extensions" version = "0.1.0" @@ -275,6 +315,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -294,6 +345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -310,18 +362,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -330,7 +370,8 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi 6.0.0", + "r-efi", + "rand_core", "wasip2", "wasip3", ] @@ -382,25 +423,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hickory-proto" -version = "0.25.2" +name = "hickory-net" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" dependencies = [ "async-trait", "bitflags", "bytes", "cfg-if", "data-encoding", - "enum-as-inner", "futures-channel", "futures-io", "futures-util", "h2", + "hickory-proto", "http", "idna", "ipnet", - "once_cell", + "jni", + "lru-cache", + "parking_lot", "rand", "ring", "rustls", @@ -412,31 +455,59 @@ dependencies = [ "tokio-rustls", "tracing", "url", - "webpki-roots 0.26.11", + "webpki-roots", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "bitflags", + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand", + "ring", + "rustls-pki-types", + "thiserror", + "time", + "tinyvec", + "tracing", + "url", ] [[package]] name = "hickory-resolver" -version = "0.25.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" dependencies = [ "cfg-if", "futures-util", + "hickory-net", "hickory-proto", "ipconfig", + "ipnet", + "jni", "moka", + "ndk-context", "once_cell", "parking_lot", "rand", "resolv-conf", "rustls", "smallvec", + "system-configuration", "thiserror", "tokio", "tokio-rustls", "tracing", - "webpki-roots 0.26.11", + "webpki-roots", ] [[package]] @@ -587,6 +658,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "itertools" @@ -603,6 +677,55 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -635,6 +758,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "litemap" version = "0.8.1" @@ -656,6 +785,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "memchr" version = "2.8.0" @@ -722,6 +860,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "nom" version = "7.1.3" @@ -738,6 +882,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -804,15 +957,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "predicates" version = "3.1.4" @@ -839,6 +983,17 @@ dependencies = [ "termtree", ] +[[package]] +name = "prefix-trie" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -867,12 +1022,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -881,32 +1030,20 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom 0.4.2", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" @@ -972,6 +1109,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.23.37" @@ -998,9 +1144,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", @@ -1013,6 +1159,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1074,6 +1229,22 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -1130,6 +1301,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -1328,6 +1520,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1431,15 +1633,6 @@ dependencies = [ "semver", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.6", -] - [[package]] name = "webpki-roots" version = "1.0.6" @@ -1455,6 +1648,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -1689,26 +1891,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zerocopy" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zerofrom" version = "0.1.6" diff --git a/EXTENSION_POLICY.md b/EXTENSION_POLICY.md index 4802e88287e7c..5fd71db99bd82 100644 --- a/EXTENSION_POLICY.md +++ b/EXTENSION_POLICY.md @@ -89,22 +89,30 @@ part of the Wasm implementation validation. The rationale for this policy: ## Extension stability and security posture -Every extension is expected to be tagged with a `status` and `security_posture` in its -`envoy_cc_extension` rule. +Every extension is expected to be tagged with a `status` and `security_posture` in its entry in the +extension metadata file +([source/extensions/extensions_metadata.yaml](source/extensions/extensions_metadata.yaml) for core +extensions, [contrib/extensions_metadata.yaml](contrib/extensions_metadata.yaml) for contrib +extensions). The schema for these files is defined in +[tools/extensions/extensions_schema.yaml](tools/extensions/extensions_schema.yaml). The `status` is one of: -* `stable`: The extension is stable and is expected to be production usable. This is the default if - no `status` is specified. +* `stable`: The extension is stable and is expected to be production usable. * `alpha`: The extension is functional but has not had substantial production burn time, use only with this caveat. * `wip`: The extension is work-in-progress. Functionality is incomplete and it is not intended for production use. +Extensions that can be used in both downstream and upstream contexts (e.g., HTTP filters listed +under both `envoy.filters.http` and `envoy.filters.http.upstream` categories) may also specify a +`status_upstream` field. This allows the upstream usage to have a different maturity level than +the downstream usage (e.g., `status: stable` with `status_upstream: alpha`). + The extension status may be adjusted by the extension [CODEOWNERS](./CODEOWNERS) and/or Envoy maintainers based on an assessment of the above criteria. Note that the status of the extension reflects the implementation status. It is orthogonal to the API stability, for example, an extension API marked with `(xds.annotations.v3.file_status).work_in_progress` might have a `stable` implementation and -and an extension with a stable config proto can have a `wip` implementation. +an extension with a stable config proto can have a `wip` implementation. The `security_posture` is one of: * `robust_to_untrusted_downstream`: The extension is hardened against untrusted downstream traffic. It diff --git a/OWNERS.md b/OWNERS.md index fbe042ac23ac7..353c51fad4cff 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -26,6 +26,8 @@ routing PRs, questions, etc. to the right place. * Upstream, LB, tracing, logging, performance, and generic/dubbo proxy. * Boteng Yao ([botengyao](https://github.com/botengyao)) (boteng@google.com) * Overload manager, security, logging, wasm, data plane. +* Rohit Agrawal ([agrawroh](https://github.com/agrawroh)) (rohit.agrawal@databricks.com) + * Dynamic Modules, Reverse Tunnels, Lua, ExtAuthZ, Matchers, CI, Dependencies, Docs. # Maintainers @@ -48,8 +50,6 @@ routing PRs, questions, etc. to the right place. * Load balancing, data plane. * Takeshi Yoneda ([mathetake](https://github.com/mathetake)) (t.y.mathetake@gmail.com) * Dynamic modules, API gateway, WASM, Istio. -* Rohit Agrawal ([agrawroh](https://github.com/agrawroh)) (rohit.agrawal@databricks.com) - * Dynamic Modules, Reverse Tunnels, Lua, ExtAuthZ, Matchers, CI, Dependencies, Docs. * Paul Ogilby ([paul-r-gall](https://github.com/paul-r-gall)) (pgal@google.com) * Request mirroring, data plane * Mike Krinkin ([krinkinmu](https://github.com/krinkinmu)) (krinkin.m.u@gmail.com) @@ -102,6 +102,11 @@ without further review. * Antonio Leonti ([antoniovleonti](https://github.com/antoniovleonti)) (leonti@google.com) * Takeshi Yoneda ([mathetake](https://github.com/mathetake)) (tyoneda@netflix.com) * Reuben Tanner ([reubent-goog](https://github.com/reubent-goog)) (reubent@google.com) +* Paul Ogilby ([paul-r-gall](https://github.com/paul-r-gall)) (pgal@google.com) +* Peng Gao ([penguingao](https://github.com/penguingao)) (pengg@google.com) +* Jonh Wendell ([jwendell](https://github.com/jwendell)) (jwendell@redhat.com) +* Adi (Suissa) Peleg ([adisuissa](https://github.com/adisuissa)) (adip@google.com) +* Ignasi Barrera ([nacx](https://github.com/nacx)) (nacx@apache.org) # Emeritus maintainers diff --git a/PULL_REQUESTS.md b/PULL_REQUESTS.md index c8d5921ee2c8c..afd0b1720b51c 100644 --- a/PULL_REQUESTS.md +++ b/PULL_REQUESTS.md @@ -65,10 +65,11 @@ Request](https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request) do ### Release notes If this change is user impacting OR extension developer impacting (filter API, etc.) you **must** -add a release note to the [version history](changelogs/current.yaml) for the -current version. Please include any relevant links. Each release note should be prefixed with the -relevant subsystem in **alphabetical order** (see existing examples as a guide) and include links -to relevant parts of the documentation. Thank you! Please write in N/A if there are no release notes. +add a release note fragment under [changelogs/current](changelogs/current) for the +current version. Choose the appropriate section directory and name the file +`__.rst`, using the canonical sections and areas listed in +[changelogs/changelogs.yaml](changelogs/changelogs.yaml). Please include any relevant links to +documentation. Thank you! Please write in N/A if there are no release notes. ### Platform Specific Features @@ -118,8 +119,7 @@ merged. ### Deprecated If this PR deprecates existing Envoy APIs or code, it should include an update to the deprecated -section of the [version history](changelogs/current.yaml) and a one line note in the -PR description. +section under [changelogs/current](changelogs/current) and a one line note in the PR description. If you mark existing APIs or code as deprecated, when the next release is cut, the deprecation script will create and assign an issue to you for diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index caaead782fead..59aa8e653d570 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,14 @@ CLIENT sidecar (OUTBOUND) tcp_proxy{tunneling_config: CONNECT} +// | HTTP/2 CONNECT +// v +// SERVER sidecar (INBOUND) HCM CONNECT-terminate -> app cluster +// v +// app (raw TCP FakeUpstream) +// +// The SERVER is BaseIntegrationTest's `test_server_` (httpProxyConfig base gives +// an HCM with a route_config that setConnectConfig() rewrites for CONNECT). The +// CLIENT is a second IntegrationTestServer seeded from a copy of that bootstrap, +// with its listener rewritten to a tunneling tcp_proxy and its cluster set to +// HTTP/2. +class IstioHboneTwoProxyIntegrationTest + : public testing::TestWithParam, + public BaseIntegrationTest { +public: + IstioHboneTwoProxyIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::httpProxyConfig()) { + enableHalfClose(true); + } + +protected: + static void clusterToHttp2(envoy::config::cluster::v3::Cluster& cluster) { + ConfigHelper::HttpProtocolOptions options; + options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + ConfigHelper::setProtocolOptions(cluster, options); + } + + static void setNode(ConfigHelper& helper, const std::string& workload, const std::string& ns, + const std::string& cluster, const std::string& canonical, + const std::string& app) { + helper.addConfigModifier([=](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& node = *bootstrap.mutable_node(); + node.set_id(absl::StrCat(workload, "-id")); + node.set_cluster(cluster); + auto& fields = *node.mutable_metadata()->mutable_fields(); + fields["WORKLOAD_NAME"].set_string_value(workload); + fields["NAMESPACE"].set_string_value(ns); + fields["CLUSTER_ID"].set_string_value(cluster); + fields["ISTIO_VERSION"].set_string_value("1.20.0"); + auto& labels = *fields["LABELS"].mutable_struct_value()->mutable_fields(); + labels[std::string(Istio::Common::CanonicalNameLabel)].set_string_value(canonical); + labels[std::string(Istio::Common::CanonicalRevisionLabel)].set_string_value("v1"); + labels[std::string(Istio::Common::AppNameLabel)].set_string_value(app); + labels[std::string(Istio::Common::AppVersionLabel)].set_string_value("v1"); + }); + } + + Stats::CounterSharedPtr waitForIstioCounter(Stats::Store& store, absl::string_view metric) { + const std::string extracted = absl::StrCat("istiocustom.", metric); + for (int i = 0; i < 100; ++i) { + for (const auto& counter : store.counters()) { + if (counter->tagExtractedName() == extracted && counter->value() > 0) { + return counter; + } + } + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(50)); + } + return nullptr; + } + + static std::optional tagValue(const Stats::Counter& counter, absl::string_view tag) { + for (const auto& t : counter.tags()) { + if (t.name_ == tag) { + return t.value_; + } + } + return std::nullopt; + } + + // Encodes a destination workload as the x-envoy-peer-metadata wire value (base64 of a + // deterministically serialized Struct) so the raw app can play the destination peer on + // its HTTP response, letting the client attribute the destination over the tunnel. + static std::string destPeerMetadataHeader() { + const Istio::Common::WorkloadMetadataObject dest( + "dest-pod", "dest-cluster", "dest-ns", "dest-v1", "dest-svc", "v1", "dest-app", "v1", + Istio::Common::WorkloadType::Pod, "spiffe://dest", "", ""); + const Protobuf::Struct md = Istio::Common::convertWorkloadMetadataToStruct(dest); + const std::string bytes = Istio::Common::serializeToStringDeterministic(md); + return Base64::encode(bytes.data(), bytes.size()); + } + + void startSidecar(ConfigHelper& helper, const std::vector& upstream_ports, + const std::string& listener_name, IntegrationTestServerPtr& out) { + helper.finalize(upstream_ports); + const std::string path = TestEnvironment::writeStringToFileForTest( + absl::StrCat("hbone_two_proxy_", listener_name, ".pb"), + TestUtility::getProtobufBinaryStringFromMessage(helper.bootstrap())); + createGeneratedApiTestServer(path, {listener_name}, {false, true, false}, false, out); + } + + void initializeHbone() { + // Snapshot the pristine bootstrap for the CLIENT before the SERVER's modifiers + // and finalize mutate config_helper_ (otherwise the client would inherit the + // server's already-resolved cluster endpoint and tunnel straight to the app). + const envoy::config::bootstrap::v3::Bootstrap pristine = config_helper_.bootstrap(); + + // 1. Raw TCP app the SERVER sidecar forwards the tunneled bytes to. + setUpstreamCount(1); + createUpstreams(); + const uint32_t app_port = fake_upstreams_[0]->localAddress()->ip()->port(); + + // 2. SERVER sidecar (INBOUND): HCM terminates CONNECT and routes to the app, + // advertising its identity as a baggage header on the CONNECT response. + setNode(config_helper_, "server-v1", "server-ns", "server-cluster", "server-svc", "server-app"); + // istio.stats on the terminating HCM emits waypoint-reporter stats for the + // CONNECT stream (the server terminating HBONE is the waypoint; reporter=waypoint + // matches TestStatsServerWaypointProxyCONNECT). Prepended first so the chain is + // [peer_metadata, istio.stats, router]. + config_helper_.prependFilter(R"EOF( +name: envoy.filters.http.istio_stats +typed_config: + "@type": type.googleapis.com/stats.PluginConfig + reporter: SERVER_GATEWAY +)EOF"); + // peer_metadata both discovers the client identity from the CONNECT *request* + // baggage (source for the server's stats) and advertises the server identity on + // the CONNECT *response* baggage (consumed by the client, runs on the encode + // path -- the authentic istio mechanism). When server_resolve_backend_ is set it + // also resolves the destination (backend) workload by the upstream host IP via the + // workload-discovery provider on the response (encode) path -- mirroring the real + // waypoint's EnableMetadataDiscovery, so istio.stats can attribute the backend. + const std::string server_upstream_discovery = server_resolve_backend_ ? R"EOF( + upstream_discovery: + - workload_discovery: {})EOF" + : ""; + config_helper_.prependFilter(absl::StrCat(R"EOF( +name: envoy.filters.http.peer_metadata +typed_config: + "@type": type.googleapis.com/io.istio.http.peer_metadata.Config + downstream_discovery: + - baggage: {} + downstream_propagation: + - baggage: {})EOF", + server_upstream_discovery, "\n")); + if (server_resolve_backend_) { + // The workload-discovery bootstrap extension subscribes to the workload API (here a + // file path_config_source, the in-test analogue of istio/proxy's go-control-plane + // LinearCache). The HTTP peer_metadata XDSMethod looks up the upstream (backend) peer + // in it by the upstream host IP. + const std::string workloads = workloads_path_; + config_helper_.addConfigModifier( + [workloads](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + TestUtility::loadFromYaml(absl::StrCat(R"EOF( +name: envoy.bootstrap.workload_discovery +typed_config: + "@type": type.googleapis.com/istio.workload.BootstrapExtension + config_source: + path_config_source: + path: ")EOF", + workloads, "\"\n"), + *bootstrap.add_bootstrap_extensions()); + }); + } + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_name("inbound"); + listener->set_traffic_direction(envoy::config::core::v3::INBOUND); + }); + config_helper_.addConfigModifier([](HttpConnectionManager& hcm) { + hcm.set_codec_type(HttpConnectionManager::HTTP2); + ConfigHelper::setConnectConfig(hcm, /*terminate_connect=*/true, /*allow_post=*/false); + }); + // cluster_0 -> app is set via finalize({app_port}); HCM CONNECT terminate sends + // the upgraded raw stream to that cluster. + startSidecar(config_helper_, {app_port}, "inbound", test_server_); + const uint32_t server_port = lookupPort("inbound"); + + // 3. CLIENT sidecar (OUTBOUND): the ambient/HBONE encap chain. + buildClientSidecar(pristine, server_port); + } + + // Builds the CLIENT sidecar (OUTBOUND) ambient/HBONE encap chain, seeded from the + // pristine server bootstrap. Shared by every HBONE test -- the client topology is + // identical regardless of what the server does with the terminated tunnel: + // outbound listener [istio.stats(network), tcp_proxy → "encap"] + // "encap" cluster → internal listener "connect_originate" + // "connect_originate" [peer_metadata, tcp_proxy{tunneling: CONNECT} → "hbone"] + // "hbone" cluster → SERVER over HTTP/2 + void buildClientSidecar(const envoy::config::bootstrap::v3::Bootstrap& pristine, + uint32_t server_port) { + const std::string loopback = Network::Test::getLoopbackAddressString(version_); + // When the client advertises its identity, the tunneling tcp_proxy adds a + // baggage header to the CONNECT request (read from peer_metadata filter state). + // Suppressing it models the waypoint "empty metadata" case (unknown source). + const std::string connect_request_baggage = client_sends_baggage_ ? R"EOF( + headers_to_add: + - header: + key: baggage + value: "%FILTER_STATE(baggage:PLAIN)%")EOF" + : ""; + client_config_ = std::make_unique(version_, pristine); + setNode(*client_config_, "client-v1", "client-ns", "client-cluster", "client-svc", + "client-app"); + client_config_->addConfigModifier([server_port, loopback, connect_request_baggage]( + envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* sr = bootstrap.mutable_static_resources(); + + // Internal-listener bootstrap extension. + envoy::extensions::bootstrap::internal_listener::v3::InternalListener il; + auto* be = bootstrap.add_bootstrap_extensions(); + be->set_name("envoy.bootstrap.internal_listener"); + std::ignore = be->mutable_typed_config()->PackFrom(il); + + // outbound listener → [istio.stats(network, source), tcp_proxy → "encap"]. + auto* outbound = sr->mutable_listeners(0); + outbound->set_name("outbound"); + outbound->set_traffic_direction(envoy::config::core::v3::OUTBOUND); + auto* ofc = outbound->mutable_filter_chains(0); + ofc->clear_filters(); + TestUtility::loadFromYaml(R"EOF( +name: envoy.filters.network.istio_stats +typed_config: + "@type": type.googleapis.com/stats.PluginConfig + tcp_reporting_duration: 1s +)EOF", + *ofc->add_filters()); + TcpProxy out_tcp; + out_tcp.set_stat_prefix("outbound_tcp"); + out_tcp.set_cluster("encap"); + auto* of = ofc->add_filters(); + of->set_name("envoy.filters.network.tcp_proxy"); + std::ignore = of->mutable_typed_config()->PackFrom(out_tcp); + + // "connect_originate" internal listener: downstream peer_metadata computes the + // local (client) baggage into filter state, which the tunneling tcp_proxy adds + // to the CONNECT request; on the response it reads the peer (server) baggage + // that tcp_proxy saved and injects it as the MX preamble downstream. + TestUtility::loadFromYaml(absl::StrCat(R"EOF( +name: connect_originate +internal_listener: {} +filter_chains: +- filters: + - name: envoy.filters.network.peer_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.network_filters.peer_metadata.Config + baggage_key: baggage + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: connect_originate + cluster: hbone + tunneling_config: + hostname: "app.default.svc.cluster.local:80" + propagate_response_headers: true)EOF", + connect_request_baggage, "\n"), + *sr->add_listeners()); + + // cluster_0 becomes "encap" → the connect_originate internal listener, with the + // upstream peer_metadata filter consuming the MX preamble into UpstreamPeer. + auto* encap = sr->mutable_clusters(0); + encap->set_name("encap"); + encap->clear_load_assignment(); + TestUtility::loadFromYaml(R"EOF( +name: encap +filters: +- name: envoy.filters.network.upstream.peer_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.network_filters.peer_metadata.UpstreamConfig +load_assignment: + cluster_name: encap + endpoints: + - lb_endpoints: + - endpoint: + address: + envoy_internal_address: + server_listener_name: connect_originate + endpoint_id: hbone +)EOF", + *encap); + + // "hbone" cluster → SERVER over HTTP/2. + auto* hbone = sr->add_clusters(); + TestUtility::loadFromYaml(fmt::format(R"EOF( +name: hbone +connect_timeout: 5s +load_assignment: + cluster_name: hbone + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: {} +)EOF", + loopback, server_port), + *hbone); + clusterToHttp2(*hbone); + }); + startSidecar(*client_config_, {}, "outbound", client_sidecar_); + } + + // Client-side L7 stats over HBONE (migrates TestStatsClientSidecarCONNECT). Unlike + // buildClientSidecar (raw TCP over the tunnel), the client OUTBOUND listener here is an + // HCM carrying the HTTP istio.stats filter, so an L7 request is recorded as + // istio_requests_total{reporter=source} before being tunneled: + // client HTTP request --> OUTBOUND HCM [istio.stats(http), router] -> cluster "encap" + // "encap" -> internal listener "connect_originate" + // "connect_originate" tcp_proxy{tunneling: CONNECT} -> "hbone" + // "hbone" -> SERVER over HTTP/2 (terminates CONNECT -> app) + // No network peer_metadata on the internal listener (its MX preamble would corrupt the + // tunneled HTTP bytes); without MX the destination peer is unknown, so this asserts the + // source-side L7 recording over CONNECT -- the point of TestStatsClientSidecarCONNECT. + void buildClientSidecarHttp(const envoy::config::bootstrap::v3::Bootstrap& pristine, + uint32_t server_port) { + const std::string loopback = Network::Test::getLoopbackAddressString(version_); + client_config_ = std::make_unique(version_, pristine); + setNode(*client_config_, "client-v1", "client-ns", "client-cluster", "client-svc", + "client-app"); + // istio.stats on the client outbound HCM records istio_requests_total{reporter=source}. + client_config_->prependFilter(R"EOF( +name: envoy.filters.http.istio_stats +typed_config: + "@type": type.googleapis.com/stats.PluginConfig +)EOF"); + // peer_metadata learns the destination (server/backend) identity from the upstream + // response's x-envoy-peer-metadata header, carried back through the tunnel. Prepended + // after istio.stats so the final chain is [peer_metadata, istio.stats, router]. + client_config_->prependFilter(R"EOF( +name: envoy.filters.http.peer_metadata +typed_config: + "@type": type.googleapis.com/io.istio.http.peer_metadata.Config + upstream_discovery: + - istio_headers: {} +)EOF"); + // Route the HCM to the "encap" cluster (the internal-listener HBONE encap path). + client_config_->addConfigModifier([](HttpConnectionManager& hcm) { + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("encap"); + }); + client_config_->addConfigModifier( + [server_port, loopback](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* sr = bootstrap.mutable_static_resources(); + + auto* outbound = sr->mutable_listeners(0); + outbound->set_name("outbound"); + outbound->set_traffic_direction(envoy::config::core::v3::OUTBOUND); + + // Internal-listener bootstrap extension. + envoy::extensions::bootstrap::internal_listener::v3::InternalListener il; + auto* be = bootstrap.add_bootstrap_extensions(); + be->set_name("envoy.bootstrap.internal_listener"); + std::ignore = be->mutable_typed_config()->PackFrom(il); + + // "connect_originate" internal listener: a tunneling tcp_proxy that wraps the + // HCM's upstream HTTP bytes in an HTTP/2 CONNECT to the SERVER. + TestUtility::loadFromYaml(R"EOF( +name: connect_originate +internal_listener: {} +filter_chains: +- filters: + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: connect_originate + cluster: hbone + tunneling_config: + hostname: "app.default.svc.cluster.local:80" +)EOF", + *sr->add_listeners()); + + // cluster_0 becomes "encap" -> the connect_originate internal listener. + auto* encap = sr->mutable_clusters(0); + encap->set_name("encap"); + encap->clear_load_assignment(); + TestUtility::loadFromYaml(R"EOF( +name: encap +load_assignment: + cluster_name: encap + endpoints: + - lb_endpoints: + - endpoint: + address: + envoy_internal_address: + server_listener_name: connect_originate + endpoint_id: hbone +)EOF", + *encap); + + // "hbone" cluster -> SERVER over HTTP/2. + auto* hbone = sr->add_clusters(); + TestUtility::loadFromYaml(fmt::format(R"EOF( +name: hbone +connect_timeout: 5s +load_assignment: + cluster_name: hbone + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: {} +)EOF", + loopback, server_port), + *hbone); + clusterToHttp2(*hbone); + }); + startSidecar(*client_config_, {}, "outbound", client_sidecar_); + } + + // Brings up a CONNECT-terminating SERVER (HCM -> app) and an HTTP-fronted CLIENT + // (buildClientSidecarHttp) for the client-side L7-over-HBONE case. + void initializeHboneClientHttp() { + const envoy::config::bootstrap::v3::Bootstrap pristine = config_helper_.bootstrap(); + + setUpstreamCount(1); + createUpstreams(); + const uint32_t app_port = fake_upstreams_[0]->localAddress()->ip()->port(); + + // SERVER (INBOUND): HCM terminates the CONNECT tunnel and forwards the inner stream + // to the app cluster. No istio filters needed -- this case asserts only the client. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_name("inbound"); + listener->set_traffic_direction(envoy::config::core::v3::INBOUND); + }); + config_helper_.addConfigModifier([](HttpConnectionManager& hcm) { + hcm.set_codec_type(HttpConnectionManager::HTTP2); + ConfigHelper::setConnectConfig(hcm, /*terminate_connect=*/true, /*allow_post=*/false); + }); + startSidecar(config_helper_, {app_port}, "inbound", test_server_); + const uint32_t server_port = lookupPort("inbound"); + + buildClientSidecarHttp(pristine, server_port); + } + + // Server-side waypoint over CONNECT, L4 variant (migrates + // TestTCPStatsServerWaypointProxyCONNECT). The waypoint terminates the HBONE + // tunnel with an HCM, learns the client identity from the CONNECT-request baggage + // via the HTTP peer_metadata filter (shared_with_upstream), and routes the inner + // upgraded stream to an internal listener that carries the *network* istio.stats + // filter -- which reads the shared downstream peer and emits istio_tcp_* with + // reporter=waypoint. This is the L4 counterpart of ServerSidecarStatsOverHbone + // (which asserts the L7 istio_requests_total{reporter=waypoint}). + // + // client CONNECT --> SERVER "inbound" HCM (terminate CONNECT) + // [peer_metadata(baggage, shared_with_upstream), router] + // route connect_matcher -> cluster "internal_inbound" + // | internal_upstream transport carries the shared + // | DownstreamPeerObj across the internal boundary + // v + // internal listener "internal_inbound" + // [istio.stats(network, reporter=waypoint), tcp_proxy] + // v + // cluster_0 -> app (raw TCP FakeUpstream) + void initializeHboneWaypointTcp() { + const envoy::config::bootstrap::v3::Bootstrap pristine = config_helper_.bootstrap(); + + setUpstreamCount(1); + createUpstreams(); + const uint32_t app_port = fake_upstreams_[0]->localAddress()->ip()->port(); + + setNode(config_helper_, "server-v1", "server-ns", "server-cluster", "server-svc", "server-app"); + // The waypoint reads the client identity from the CONNECT-request baggage and marks + // the peer filter state shared_with_upstream so it crosses the internal boundary to + // the network istio.stats filter. No HTTP istio.stats here -- the TCP variant reports + // istio_tcp_* from the network filter on the internal listener. + config_helper_.prependFilter(R"EOF( +name: envoy.filters.http.peer_metadata +typed_config: + "@type": type.googleapis.com/io.istio.http.peer_metadata.Config + downstream_discovery: + - baggage: {} + downstream_propagation: + - baggage: {} + shared_with_upstream: true +)EOF"); + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* sr = bootstrap.mutable_static_resources(); + + // Internal-listener bootstrap extension (the waypoint routes the inner stream + // through an internal listener). + envoy::extensions::bootstrap::internal_listener::v3::InternalListener il; + auto* be = bootstrap.add_bootstrap_extensions(); + be->set_name("envoy.bootstrap.internal_listener"); + std::ignore = be->mutable_typed_config()->PackFrom(il); + + auto* listener = sr->mutable_listeners(0); + listener->set_name("inbound"); + listener->set_traffic_direction(envoy::config::core::v3::INBOUND); + + // "internal_inbound" cluster -> the internal listener, with the internal_upstream + // transport so shared_with_upstream filter state (the discovered downstream peer) + // is carried across the internal boundary. + auto* ic = sr->add_clusters(); + TestUtility::loadFromYaml(R"EOF( +name: internal_inbound +connect_timeout: 5s +load_assignment: + cluster_name: internal_inbound + endpoints: + - lb_endpoints: + - endpoint: + address: + envoy_internal_address: + server_listener_name: internal_inbound +transport_socket: + name: envoy.transport_sockets.internal_upstream + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.internal_upstream.v3.InternalUpstreamTransport + transport_socket: + name: envoy.transport_sockets.raw_buffer + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.raw_buffer.v3.RawBuffer +)EOF", + *ic); + + // Internal listener "internal_inbound": [istio.stats(network, waypoint), tcp_proxy]. + TestUtility::loadFromYaml(R"EOF( +name: internal_inbound +internal_listener: {} +traffic_direction: INBOUND +filter_chains: +- filters: + - name: envoy.filters.network.istio_stats + typed_config: + "@type": type.googleapis.com/stats.PluginConfig + reporter: SERVER_GATEWAY + tcp_reporting_duration: 1s + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: internal_inbound + cluster: cluster_0 +)EOF", + *sr->add_listeners()); + }); + config_helper_.addConfigModifier([](HttpConnectionManager& hcm) { + hcm.set_codec_type(HttpConnectionManager::HTTP2); + ConfigHelper::setConnectConfig(hcm, /*terminate_connect=*/true, /*allow_post=*/false); + // Route the terminated CONNECT stream to the internal listener (not straight to + // the app cluster), so it traverses the network istio.stats filter. + hcm.mutable_route_config() + ->mutable_virtual_hosts(0) + ->mutable_routes(0) + ->mutable_route() + ->set_cluster("internal_inbound"); + }); + startSidecar(config_helper_, {app_port}, "inbound", test_server_); + const uint32_t server_port = lookupPort("inbound"); + + buildClientSidecar(pristine, server_port); + } + + // When false, the client omits its baggage on the CONNECT request, modeling the + // waypoint "empty metadata" case (the waypoint sees an unknown source). + bool client_sends_baggage_{true}; + // When true, the SERVER (waypoint) resolves the destination (backend) workload by the + // upstream host IP via the workload-discovery provider reading workloads_path_. + bool server_resolve_backend_{false}; + std::string workloads_path_; + std::unique_ptr client_config_; + IntegrationTestServerPtr client_sidecar_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, IstioHboneTwoProxyIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Related upstream e2e: stats_plugin CONNECT/HBONE cases as shared tunnel scaffolding. +// Raw bytes flow through the CONNECT tunnel end to end. +TEST_P(IstioHboneTwoProxyIntegrationTest, ConnectTunnelBytesFlow) { + initializeHbone(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + // The inner payload (not the HTTP/2 CONNECT frames) reaches the app, proving the + // server terminated the tunnel; bytes flow back through it too. + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); +} + +// Related upstream e2e: stats_plugin/TestTCPStatsServerWaypointProxyCONNECT (client/HBONE +// transport side of the CONNECT flow). +// The CLIENT sidecar emits istio_tcp_* over the full HBONE encap +// (internal listener "connect_originate" + tunneling tcp_proxy + the network +// peer_metadata filters), with reporter=source, the local (client) identity, and +// the destination (server) identity learned end to end over the tunnel: +// server HTTP peer_metadata advertises its baggage on the CONNECT response -> +// client tcp_proxy saves it as TunnelResponseHeaders -> downstream peer_metadata +// injects the MX preamble -> upstream peer_metadata consumes it into the peer +// filter state -> istio.stats reads it. +// +// Closing this required a one-line fix to the network peer_metadata UpstreamFilter +// (peer_metadata.cc): it now also stores the WorkloadMetadataObject under +// UpstreamPeerObj (mirroring the metadata_exchange filter). Previously it stored +// only a CelState whose struct keys are incompatible with istio.stats' CelState +// fallback, so the peer identity was lost over HBONE. +TEST_P(IstioHboneTwoProxyIntegrationTest, ClientSidecarTcpStatsOverHbone) { + initializeHbone(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); + + // istio.stats emits TCP counters over the HBONE tunnel with the source identity. + auto client = + waitForIstioCounter(client_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*client, "destination_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsClientSidecarCONNECT. +// Migrates TestStatsClientSidecarCONNECT: the CLIENT sidecar records L7 +// istio_requests_total{reporter=source} for an HTTP request that is encapsulated into an +// HBONE CONNECT tunnel. The outbound HCM (with the HTTP peer_metadata + istio.stats +// filters) handles the request, then the tunneling tcp_proxy on the internal listener +// wraps the upstream HTTP bytes in a CONNECT to the SERVER, which terminates the tunnel +// and forwards the inner HTTP stream to the raw app. The app plays the destination peer by +// returning an x-envoy-peer-metadata response header; it travels back through the tunnel +// and the client peer_metadata filter reads it, so the client attributes BOTH the source +// (its node) and the destination (dest-v1) for the request carried over CONNECT. +TEST_P(IstioHboneTwoProxyIntegrationTest, ClientSidecarHttpRequestsTotalOverHbone) { + initializeHboneClientHttp(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE( + tcp_client->write("GET / HTTP/1.1\r\nhost: server-svc.server-ns.svc.cluster.local\r\n\r\n")); + + // The inner HTTP request reaches the app (the SERVER terminated CONNECT); the app + // replies with a raw HTTP/1 response carrying its peer metadata, which flows back + // through the tunnel to the client. + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + // waitForData(uint64_t) matches an exact byte count; the inner request size is not + // known up front, so wait for the request line via the inexact-match validator. + ASSERT_TRUE(app_conn->waitForData(FakeRawConnection::waitForInexactMatch("GET /"))); + ASSERT_TRUE(app_conn->write(absl::StrCat("HTTP/1.1 200 OK\r\n" + "x-envoy-peer-metadata-id: dest-id\r\n" + "x-envoy-peer-metadata: ", + destPeerMetadataHeader(), + "\r\n" + "content-length: 0\r\n\r\n"))); + tcp_client->waitForData("200", /*exact_match=*/false); + tcp_client->close(); + + // The client recorded the L7 request as a source-reporter stat over the CONNECT path, + // with the destination attributed from the response peer metadata carried back through + // the tunnel. + auto client = waitForIstioCounter(client_sidecar_->statStore(), "istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client, "source_workload").value_or("")); + EXPECT_EQ("dest-v1", tagValue(*client, "destination_workload").value_or("")); + EXPECT_EQ("http", tagValue(*client, "request_protocol").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsServerWaypointProxyCONNECT (full-metadata case). +// Server-side waypoint over HBONE: the SERVER (INBOUND) terminates the CONNECT +// tunnel, reads the client identity from the CONNECT-request baggage via the HTTP +// peer_metadata filter, and emits waypoint-reporter stats for the stream (the +// server terminating HBONE is the waypoint, matching TestStatsServerWaypointProxy +// CONNECT). reporter=waypoint, source=client (from the CONNECT-request baggage). +// The destination (backend) is the raw-TCP app, which provides no identity here, so +// destination_workload resolves to "unknown" (a real waypoint's backend would +// supply it via endpoint/peer metadata). +TEST_P(IstioHboneTwoProxyIntegrationTest, ServerSidecarStatsOverHbone) { + initializeHbone(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); + + // The waypoint (test_server_) terminated the tunnel and recorded the request with + // the client identity learned from the CONNECT-request baggage. + auto server = waitForIstioCounter(test_server_->statStore(), "istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsServerWaypointProxyCONNECT (full-metadata, +// backend-attribution side). +// Like ServerSidecarStatsOverHbone, but the waypoint additionally attributes the +// destination (backend) workload. Upstream's full-metadata case supplies the backend via +// UpdateWorkloadMetadata keyed by the backend IP and asserts the destination_* tags; here +// the workload-discovery provider is fed a Workload for the app's loopback IP, and the HTTP +// peer_metadata upstream workload_discovery resolves it on the CONNECT response (encode) +// path -- by then the upstream (app) connection is established, so the upstream host IP is +// known. istio.stats(SERVER_GATEWAY) then reads UpstreamPeerObj for the destination tags. +TEST_P(IstioHboneTwoProxyIntegrationTest, ServerWaypointBackendDestinationOverHbone) { + // Backend (destination) workload keyed by the app's loopback IP (raw network-order + // address bytes, as the workload-discovery provider reconstructs them: "fwAAAQ==" is + // {127,0,0,1} and the long form is ::1 = 15 zero bytes + 0x01). canonical_name doubles + // as app name and canonical_revision as version in the workload->metadata conversion. + workloads_path_ = TestEnvironment::writeStringToFileForTest("hbone_backend_workloads.yaml", + R"EOF( +resources: +- "@type": type.googleapis.com/istio.workload.Workload + uid: backend-uid + name: ratings-pod + workload_name: ratings-v1 + namespace: bookinfo + cluster_id: backend-cluster + canonical_name: ratings + canonical_revision: v1 + addresses: + - "fwAAAQ==" + - "AAAAAAAAAAAAAAAAAAAAAQ==" +)EOF", + /*fully_qualified_path=*/false); + server_resolve_backend_ = true; + initializeHbone(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); + + auto server = waitForIstioCounter(test_server_->statStore(), "istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + // Source (client) learned from the CONNECT-request baggage. + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + // Destination (backend) resolved from the workload-discovery provider by the upstream + // host IP -- the attribution upstream's full-metadata case validates. + EXPECT_EQ("ratings-v1", tagValue(*server, "destination_workload").value_or("")); + EXPECT_EQ("bookinfo", tagValue(*server, "destination_workload_namespace").value_or("")); + EXPECT_EQ("ratings", tagValue(*server, "destination_canonical_service").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsServerWaypointProxyCONNECT (empty-metadata case). +// migrates TestStatsServerWaypointProxyCONNECT/empty_metadata: when the client sends +// no baggage on the CONNECT request, the waypoint records the stream with an unknown +// source (it still reports reporter=waypoint). +TEST_P(IstioHboneTwoProxyIntegrationTest, ServerWaypointStatsEmptyMetadata) { + client_sends_baggage_ = false; + initializeHbone(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); + + auto server = waitForIstioCounter(test_server_->statStore(), "istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("unknown", tagValue(*server, "source_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestTCPStatsServerWaypointProxyCONNECT. +// Migrates TestTCPStatsServerWaypointProxyCONNECT: the waypoint emits istio_tcp_* +// (L4) for the CONNECT-tunneled stream, with reporter=waypoint, the source identity +// learned from the CONNECT-request baggage (crossing the internal-listener boundary +// via shared_with_upstream), and the destination being the waypoint's own identity. +TEST_P(IstioHboneTwoProxyIntegrationTest, ServerWaypointTcpStatsOverHbone) { + initializeHboneWaypointTcp(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); + + // The network istio.stats on the waypoint's internal listener recorded the TCP + // connection with the client identity (shared across the internal boundary) and the + // waypoint reporter -- the novel L4-over-CONNECT mechanism this test exists to prove. + auto server = + waitForIstioCounter(test_server_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("tcp", tagValue(*server, "request_protocol").value_or("")); + // For a waypoint (ServerGateway reporter) destination_workload is the backend the + // waypoint fronts, read from UpstreamPeerObj on the internal listener's *downstream* + // connection filter state (istio_stats.cc reads info.filterState(), not the upstream + // filter state, for ServerGateway). Nothing can write it there in this fixture: + // - A downstream network metadata_exchange (enable_discovery) resolves the backend in + // onData, which runs BEFORE tcp_proxy establishes the upstream -- so upstreamInfo() + // is still empty and there is no upstream host IP to resolve (unlike the L7 path, + // where peer_metadata runs in encodeHeaders, after the upstream is connected; that is + // why ServerWaypointBackendDestinationOverHbone above CAN attribute the backend). + // - An upstream metadata_exchange writes UpstreamPeerObj to the *upstream* connection's + // filter state, which ServerGateway never reads. + // - istio/proxy's real path (original_dst + distinct backend IPs + the gRPC workload + // API resolving the destination on the upstream internal listener) can't be + // reproduced in this in-process fixture, and placing a network filter on the + // internal-listener paired connection segfaults during internal-connection creation. + // So the destination stays unknown here; the source side -- the genuinely novel part + // (downstream peer crossing the internal-listener boundary on an L4 waypoint over + // CONNECT) -- is fully asserted above. + EXPECT_EQ("unknown", tagValue(*server, "destination_workload").value_or("")); +} + +} // namespace +} // namespace Envoy diff --git a/contrib/istio/filters/common/test/istio_tcp_two_proxy_integration_test.cc b/contrib/istio/filters/common/test/istio_tcp_two_proxy_integration_test.cc new file mode 100644 index 0000000000000..d86b44b5cd990 --- /dev/null +++ b/contrib/istio/filters/common/test/istio_tcp_two_proxy_integration_test.cc @@ -0,0 +1,507 @@ +#include +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/listener/v3/listener_components.pb.h" + +#include "source/common/protobuf/utility.h" + +#include "test/config/utility.h" +#include "test/integration/base_integration_test.h" +#include "test/integration/fake_upstream.h" +#include "test/integration/integration_tcp_client.h" +#include "test/integration/server.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "absl/strings/match.h" +#include "absl/strings/str_cat.h" +#include "contrib/envoy/extensions/filters/network/metadata_exchange/v3/metadata_exchange.pb.h" +#include "contrib/istio/filters/common/source/metadata_object.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +// Two-Envoy TCP fixture for the Istio sidecar stack. +// +// raw TCP client --> CLIENT sidecar (OUTBOUND, reporter=source) +// | TLS + ALPN istio2, upstream metadata_exchange +// v +// SERVER sidecar (INBOUND, reporter=destination) +// | downstream metadata_exchange + istio.stats +// v +// app (raw TCP FakeUpstream) +// +// This is the topology the earlier single-Envoy self-chain TCP test could not +// drive (its connection stalled before close); two real proxies let the real MX +// filters do the istio2 framing and the connection close propagates naturally. +class IstioTcpTwoProxyIntegrationTest : public testing::TestWithParam, + public BaseIntegrationTest { +public: + IstioTcpTwoProxyIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::tcpProxyConfig()) {} + +protected: + static envoy::config::listener::v3::Filter networkFilter(const std::string& yaml) { + envoy::config::listener::v3::Filter filter; + TestUtility::loadFromYaml(yaml, filter); + return filter; + } + + static std::string statsFilterYaml(const std::string& extra = "") { + return absl::StrCat(R"EOF( +name: envoy.filters.network.istio_stats +typed_config: + "@type": type.googleapis.com/stats.PluginConfig + tcp_reporting_duration: 1s +)EOF", + extra); + } + + static std::string tcpProxyFilterYaml() { + return R"EOF( +name: tcp +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcpproxy_stats + cluster: cluster_0 +)EOF"; + } + + std::string metadataExchangeYaml(const std::string& name, const std::string& protocol) const { + envoy::tcp::metadataexchange::config::MetadataExchange config; + config.set_protocol(protocol); + config.set_enable_discovery(enable_metadata_discovery_); + for (const auto& label : mx_additional_labels_) { + config.add_additional_labels(label); + } + + envoy::config::listener::v3::Filter filter; + filter.set_name(name); + std::ignore = filter.mutable_typed_config()->PackFrom(config); + return MessageUtil::getJsonStringFromMessageOrError(filter); + } + + static void setNode(ConfigHelper& helper, const std::string& workload, const std::string& ns, + const std::string& cluster, const std::string& canonical_name, + const std::string& app) { + helper.addConfigModifier([=](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& node = *bootstrap.mutable_node(); + node.set_id(absl::StrCat(workload, "-id")); + node.set_cluster(cluster); + auto& fields = *node.mutable_metadata()->mutable_fields(); + fields["WORKLOAD_NAME"].set_string_value(workload); + fields["NAMESPACE"].set_string_value(ns); + fields["CLUSTER_ID"].set_string_value(cluster); + fields["ISTIO_VERSION"].set_string_value("1.20.0"); + auto& labels = *fields["LABELS"].mutable_struct_value()->mutable_fields(); + labels[std::string(Istio::Common::CanonicalNameLabel)].set_string_value(canonical_name); + labels[std::string(Istio::Common::CanonicalRevisionLabel)].set_string_value("v1"); + labels[std::string(Istio::Common::AppNameLabel)].set_string_value(app); + labels[std::string(Istio::Common::AppVersionLabel)].set_string_value("v1"); + }); + } + + void startSidecar(ConfigHelper& helper, const std::vector& upstream_ports, + const std::string& listener_name, IntegrationTestServerPtr& out) { + helper.finalize(upstream_ports); + const std::string path = TestEnvironment::writeStringToFileForTest( + absl::StrCat("tcp_two_proxy_", listener_name, ".pb"), + TestUtility::getProtobufBinaryStringFromMessage(helper.bootstrap())); + createGeneratedApiTestServer(path, {listener_name}, {false, true, false}, false, out); + } + + void initializeTcpProxies() { + const std::string certs = TestEnvironment::runfilesPath("test/config/integration/certs"); + const std::string downstream_tls = fmt::format(R"EOF( +name: envoy.transport_sockets.tls +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + alpn_protocols: + - istio2 + tls_certificates: + - certificate_chain: + filename: "{}/servercert.pem" + private_key: + filename: "{}/serverkey.pem" +)EOF", + certs, certs); + const std::string upstream_tls = R"EOF( +name: envoy.transport_sockets.tls +typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + alpn_protocols: + - istio2 +)EOF"; + + // 1. Raw TCP app the SERVER sidecar forwards to. + setUpstreamCount(1); + createUpstreams(); + const uint32_t app_port = fake_upstreams_[0]->localAddress()->ip()->port(); + + // 2. SERVER sidecar (INBOUND): TLS+ALPN istio2 listener with + // [metadata_exchange(downstream), istio.stats, tcp_proxy] -> app. + server_config_ = std::make_unique(version_, config_helper_.bootstrap()); + setNode(*server_config_, "server-v1", "server-ns", "server-cluster", "server-svc", + "server-app"); + const std::string downstream_mx = + metadataExchangeYaml("envoy.filters.network.metadata_exchange", mx_protocol_); + const std::string server_stats = statsFilterYaml(server_stats_extra_); + server_config_->addConfigModifier([downstream_tls, downstream_mx, server_stats]( + envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_name("inbound"); + listener->set_traffic_direction(envoy::config::core::v3::INBOUND); + auto* fc = listener->mutable_filter_chains(0); + fc->clear_filters(); + *fc->add_filters() = networkFilter(downstream_mx); + *fc->add_filters() = networkFilter(server_stats); + *fc->add_filters() = networkFilter(tcpProxyFilterYaml()); + TestUtility::loadFromYaml(downstream_tls, *fc->mutable_transport_socket()); + }); + if (enable_metadata_discovery_) { + // The workload-discovery bootstrap extension subscribes to the workload API + // (here a file path_config_source -- the in-test analogue of istio/proxy's + // go-control-plane LinearCache); metadata_exchange's enable_discovery falls + // back to it to resolve the peer by source IP when ALPN MX is unavailable. + const std::string workloads = workloads_path_; + server_config_->addConfigModifier( + [workloads](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + TestUtility::loadFromYaml(absl::StrCat(R"EOF( +name: envoy.bootstrap.workload_discovery +typed_config: + "@type": type.googleapis.com/istio.workload.BootstrapExtension + config_source: + path_config_source: + path: ")EOF", + workloads, "\"\n"), + *bootstrap.add_bootstrap_extensions()); + }); + } + startSidecar(*server_config_, {app_port}, "inbound", server_sidecar_); + const uint32_t server_port = lookupPort("inbound"); + + // 3. CLIENT sidecar (OUTBOUND): plaintext listener with [istio.stats, tcp_proxy]; + // cluster -> SERVER sidecar over TLS+ALPN istio2 with an upstream MX filter. + setNode(config_helper_, "client-v1", "client-ns", "client-cluster", "client-svc", "client-app"); + if (!client_role_.empty()) { + const std::string role = client_role_; + config_helper_.addConfigModifier([role](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& labels = *(*bootstrap.mutable_node()->mutable_metadata()->mutable_fields())["LABELS"] + .mutable_struct_value() + ->mutable_fields(); + labels["role"].set_string_value(role); + }); + } + const std::string upstream_mx = + metadataExchangeYaml("envoy.filters.network.upstream.metadata_exchange", mx_protocol_); + config_helper_.addConfigModifier( + [upstream_tls, upstream_mx](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_name("outbound"); + listener->set_traffic_direction(envoy::config::core::v3::OUTBOUND); + auto* fc = listener->mutable_filter_chains(0); + fc->clear_filters(); + *fc->add_filters() = networkFilter(statsFilterYaml()); + *fc->add_filters() = networkFilter(tcpProxyFilterYaml()); + + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + TestUtility::loadFromYaml(upstream_tls, *cluster->mutable_transport_socket()); + envoy::config::cluster::v3::Filter mx; + TestUtility::loadFromYaml(upstream_mx, mx); + *cluster->add_filters() = mx; + }); + startSidecar(config_helper_, {server_port}, "outbound", test_server_); + } + + // Finds the single istiocustom counter for `metric` in `store`, polling until it + // appears with a non-zero value (TCP stats emit on the report timer / on close). + Stats::CounterSharedPtr waitForIstioCounter(Stats::Store& store, absl::string_view metric) { + const std::string extracted = absl::StrCat("istiocustom.", metric); + for (int i = 0; i < 100; ++i) { + for (const auto& counter : store.counters()) { + if (counter->tagExtractedName() == extracted && counter->value() > 0) { + return counter; + } + } + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(50)); + } + return nullptr; + } + + static std::optional tagValue(const Stats::Counter& counter, absl::string_view tag) { + for (const auto& t : counter.tags()) { + if (t.name_ == tag) { + return t.value_; + } + } + return std::nullopt; + } + + // Waits for a non-zero counter whose name contains `substr`, e.g. the + // metadata_exchange filter's own telemetry `metadata_exchange.*` (the listener + // scope may add a prefix, so match by substring). + Stats::CounterSharedPtr waitForCounterContaining(Stats::Store& store, absl::string_view substr) { + for (int i = 0; i < 100; ++i) { + for (const auto& c : store.counters()) { + if (absl::StrContains(c->name(), substr) && c->value() > 0) { + return c; + } + } + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(50)); + } + return nullptr; + } + + // Drives one TCP connection through both sidecars: write, echo back, close. + void driveTcpConnection() { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->waitForDisconnect()); + } + + // The ALPN protocol the metadata_exchange filter matches on. The TLS hop always + // negotiates ALPN "istio2"; setting this to something else makes the filter see + // a mismatch (alpn_protocol_not_found, no peer exchanged). + std::string mx_protocol_{"istio2"}; + // For TestTCPMXAdditionalLabels: node LABELS keys to carry as extra peer labels, + // an extra istio.stats body (a `role` dimension) on the server, and the client's + // `role` label value. + std::vector mx_additional_labels_; + std::string server_stats_extra_; + std::string client_role_; + // For TestTCPMetadataExchange/true (WDS fallback): enable metadata_exchange + // discovery and the workload_discovery bootstrap extension reading this file. + bool enable_metadata_discovery_{false}; + std::string workloads_path_; + std::unique_ptr server_config_; + IntegrationTestServerPtr server_sidecar_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, IstioTcpTwoProxyIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetadataExchange. +// One TCP connection through both sidecars; the istio2 metadata-exchange handshake +// runs over the TLS hop. Asserts istio_tcp_connections_opened_total on both +// sidecars with the reporter and (cross) peer identities each side should see. +TEST_P(IstioTcpTwoProxyIntegrationTest, TcpStatsBothSidecars) { + initializeTcpProxies(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(app_conn->waitForDisconnect()); + + // SERVER sidecar (INBOUND): reporter=destination, source identity from the + // downstream (client) MX peer, destination identity from the server node. + auto server = + waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server, "destination_workload").value_or("")); + + // CLIENT sidecar (OUTBOUND): reporter=source, destination identity from the + // upstream (server) MX peer, source identity from the client node. + auto client = + waitForIstioCounter(test_server_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*client, "destination_workload").value_or("")); + + // The connection closed cleanly through both sidecars (the exact teardown the + // earlier single-Envoy self-chain could not drive), and bytes were counted on BOTH + // sidecars (upstream TestTCPMetadataExchange validates byte counters on each side). + EXPECT_NE(waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_closed_total"), + nullptr); + EXPECT_NE(waitForIstioCounter(test_server_->statStore(), "istio_tcp_connections_closed_total"), + nullptr); + EXPECT_NE(waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_received_bytes_total"), + nullptr); + EXPECT_NE(waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_sent_bytes_total"), + nullptr); + EXPECT_NE(waitForIstioCounter(test_server_->statStore(), "istio_tcp_received_bytes_total"), + nullptr); + EXPECT_NE(waitForIstioCounter(test_server_->statStore(), "istio_tcp_sent_bytes_total"), nullptr); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetadataExchange (ALPN-found branch). +// migrates tcp_metadata_exchange: ALPN matches the MX protocol -> the network +// metadata_exchange filter exchanges peer metadata, incrementing its own +// alpn_protocol_found + metadata_added telemetry. +TEST_P(IstioTcpTwoProxyIntegrationTest, MetadataExchangeProtocolFound) { + initializeTcpProxies(); + driveTcpConnection(); + + EXPECT_NE(waitForCounterContaining(server_sidecar_->statStore(), + "metadata_exchange.alpn_protocol_found"), + nullptr); + EXPECT_NE( + waitForCounterContaining(server_sidecar_->statStore(), "metadata_exchange.metadata_added"), + nullptr); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetadataExchangeNoAlpn and +// tcp_metadata_exchange/TestTCPMetadataNotFoundReporting. +// migrates tcp_metadata_exchange (no-ALPN/`some-protocol` case): when the MX +// filter's protocol does not match the negotiated ALPN (istio2), no metadata is +// exchanged -> alpn_protocol_not_found, and the TCP stats carry an unknown peer. +TEST_P(IstioTcpTwoProxyIntegrationTest, MetadataExchangeProtocolNotFound) { + mx_protocol_ = "some-protocol"; // != the negotiated ALPN "istio2" + initializeTcpProxies(); + driveTcpConnection(); + + EXPECT_NE(waitForCounterContaining(server_sidecar_->statStore(), + "metadata_exchange.alpn_protocol_not_found"), + nullptr); + auto server = + waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("unknown", tagValue(*server, "source_workload").value_or("")); + // migrates TestTCPMetadataNotFoundReporting: with no MX, the client/source side + // also reports an unknown destination peer. + auto client = + waitForIstioCounter(test_server_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("unknown", tagValue(*client, "destination_workload").value_or("")); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetricsDuringActiveConnection. +// migrates TestTCPMetricsDuringActiveConnection (regression istio/istio#59183): the +// peer object is set for TCP, so istio_tcp_* metrics are emitted on the report timer +// *while the connection is still open*, not only at close (end_stream). +TEST_P(IstioTcpTwoProxyIntegrationTest, TcpMetricsDuringActiveConnection) { + initializeTcpProxies(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + ASSERT_TRUE(app_conn->write("world")); + tcp_client->waitForData("world"); + + // Connection still OPEN (not closed): the 1s tcp_reporting_duration timer must + // emit received-bytes with the peer identity mid-connection. + auto server = waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_received_bytes_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + + tcp_client->close(); + ASSERT_TRUE(app_conn->close()); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetadataExchangeWithConnectionTermination. +// migrates TestTCPMetadataExchangeWithConnectionTermination: the server app terminates +// the connection abruptly (right after accept, before echoing) while the istio2 MX +// handshake has completed. The server sidecar must still record the opened and closed +// connection counters with the (known) source peer -- i.e. an early server-side close +// does not lose the TCP stats. +TEST_P(IstioTcpTwoProxyIntegrationTest, ConnectionTerminationStillReportsStats) { + initializeTcpProxies(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("outbound")); + ASSERT_TRUE(tcp_client->write("hello")); + FakeRawConnectionPtr app_conn; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(app_conn)); + ASSERT_TRUE(app_conn->waitForData(5)); + // Server side terminates the connection instead of echoing. + ASSERT_TRUE(app_conn->close()); + tcp_client->waitForDisconnect(); + + // MX completed over istio2, so the source identity is known, and both the opened and + // closed counters are emitted despite the abrupt termination. + auto opened = + waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(opened, nullptr); + EXPECT_EQ("destination", tagValue(*opened, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*opened, "source_workload").value_or("")); + EXPECT_NE(waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_closed_total"), + nullptr); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMXAdditionalLabels. +// migrates TestTCPMXAdditionalLabels: with metadata_exchange `additional_labels`, an +// extra node label (`role`) is carried in the exchanged peer metadata and surfaced +// as a stats dimension on the server. +TEST_P(IstioTcpTwoProxyIntegrationTest, MetadataExchangeAdditionalLabels) { + mx_additional_labels_ = {"role"}; + client_role_ = "ingress"; + server_stats_extra_ = R"EOF( metrics: + - dimensions: + role: "filter_state.downstream_peer.labels['role']" +)EOF"; + initializeTcpProxies(); + driveTcpConnection(); + + auto server = + waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("ingress", tagValue(*server, "role").value_or("")); +} + +// Related upstream e2e: tcp_metadata_exchange/TestTCPMetadataExchange (WDS-discovery branch). +// migrates TestTCPMetadataExchange/true (WDS discovery fallback): with ALPN MX +// unavailable but enable_discovery on, metadata_exchange resolves the peer from the +// workload-discovery provider by source IP. The provider is fed a Workload resource +// (client identity at the loopback source IP) via a file path_config_source. +TEST_P(IstioTcpTwoProxyIntegrationTest, MetadataExchangeWdsFallback) { + if (version_ == Network::Address::IpVersion::v6) { + // The lookup is by raw address bytes; only the IPv4 loopback (127.0.0.1) byte + // form is pinned here. + GTEST_SKIP() << "WDS fallback test covers IPv4 loopback"; + } + // Client identity keyed by the loopback source IP 127.0.0.1 (addresses is bytes; + // "fwAAAQ==" is base64 of {127,0,0,1}). + workloads_path_ = TestEnvironment::writeStringToFileForTest("workloads.yaml", R"EOF( +resources: +- "@type": type.googleapis.com/istio.workload.Workload + uid: client-uid + name: client-pod + workload_name: client-v1 + namespace: client-ns + cluster_id: client-cluster + canonical_name: client-svc + canonical_revision: v1 + addresses: + - "fwAAAQ==" +)EOF", + /*fully_qualified_path=*/false); + enable_metadata_discovery_ = true; + mx_protocol_ = "some-protocol"; // ALPN mismatch -> MX unavailable -> WDS fallback. + initializeTcpProxies(); + driveTcpConnection(); + + EXPECT_NE(waitForCounterContaining(server_sidecar_->statStore(), + "metadata_exchange.alpn_protocol_not_found"), + nullptr); + // Source identity resolved from the workload provider (not ALPN MX). + auto server = + waitForIstioCounter(server_sidecar_->statStore(), "istio_tcp_connections_opened_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); +} + +} // namespace +} // namespace Envoy diff --git a/contrib/istio/filters/common/test/istio_two_proxy_integration_base.h b/contrib/istio/filters/common/test/istio_two_proxy_integration_base.h new file mode 100644 index 0000000000000..bd870867f8c6f --- /dev/null +++ b/contrib/istio/filters/common/test/istio_two_proxy_integration_base.h @@ -0,0 +1,356 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" + +#include "source/common/common/base64.h" +#include "source/common/protobuf/utility.h" + +#include "test/config/utility.h" +#include "test/integration/http_integration.h" +#include "test/integration/server.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_cat.h" +#include "contrib/envoy/extensions/filters/http/peer_metadata/v3/peer_metadata.pb.h" +#include "contrib/istio/filters/common/source/metadata_object.h" +#include "gtest/gtest.h" + +namespace Envoy { + +using Istio::Common::WorkloadMetadataObject; +using Istio::Common::WorkloadType; + +// Two-Envoy integration fixture for the Istio sidecar stack. +// +// Topology (HTTP/1 throughout): +// +// test client --> CLIENT sidecar (OUTBOUND, reporter=source) +// | request carries x-envoy-peer-metadata (client identity) +// v +// SERVER sidecar (INBOUND, reporter=destination) +// | response carries x-envoy-peer-metadata (server identity) +// v +// app (FakeUpstream = fake_upstreams_[0]) +// +// The CLIENT sidecar is BaseIntegrationTest's `test_server_` (the traffic entry +// point, with the downstream codec client). The SERVER sidecar is a second +// IntegrationTestServer built from its own ConfigHelper and cross-wired so the +// CLIENT sidecar's cluster targets the SERVER sidecar's inbound listener. +class IstioTwoProxyIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + IstioTwoProxyIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + +protected: + // Sets the bootstrap node metadata the istio.stats Context and peer_metadata + // propagation read (WORKLOAD_NAME, NAMESPACE, CLUSTER_ID, ISTIO_VERSION, LABELS). + static void setSidecarNode(ConfigHelper& helper, const std::string& workload, + const std::string& ns, const std::string& cluster, + const std::string& canonical_name, + const std::string& canonical_revision, const std::string& app, + const std::string& version) { + helper.addConfigModifier([=](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& node = *bootstrap.mutable_node(); + node.set_id(absl::StrCat(workload, "-id")); + node.set_cluster(cluster); + auto& fields = *node.mutable_metadata()->mutable_fields(); + fields["WORKLOAD_NAME"].set_string_value(workload); + fields["NAMESPACE"].set_string_value(ns); + fields["CLUSTER_ID"].set_string_value(cluster); + fields["ISTIO_VERSION"].set_string_value("1.20.0"); + auto& labels = *fields["LABELS"].mutable_struct_value()->mutable_fields(); + labels[std::string(Istio::Common::CanonicalNameLabel)].set_string_value(canonical_name); + labels[std::string(Istio::Common::CanonicalRevisionLabel)].set_string_value( + canonical_revision); + labels[std::string(Istio::Common::AppNameLabel)].set_string_value(app); + labels[std::string(Istio::Common::AppVersionLabel)].set_string_value(version); + }); + } + + static void setTrafficDirection(ConfigHelper& helper, + envoy::config::core::v3::TrafficDirection direction, + const std::string& listener_name) { + helper.addConfigModifier( + [direction, listener_name](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->set_name(listener_name); + listener->set_traffic_direction(direction); + }); + } + + // Builds the per-side HCM filter chain by prepending (reverse order so the final + // chain is [peer_metadata, extra..., istio.stats, router]): + // - istio.stats with the side's optional extra PluginConfig body, + // - any side-specific extra filters (e.g. set_filter_state, grpc_stats), + // - peer_metadata configured for the side's discovery/propagation. + void prependSidecarFilters(ConfigHelper& helper, bool is_client) { + // The istio.stats slot: a full filter YAML override (e.g. config_discovery / + // ECDS) if provided for this side, else the inline PluginConfig + extra body. + const std::string& override_yaml = + is_client ? client_stats_filter_override_ : server_stats_filter_override_; + if (!override_yaml.empty()) { + helper.prependFilter(override_yaml); + } else { + const std::string& stats_body = is_client ? client_stats_body_ : server_stats_body_; + helper.prependFilter(absl::StrCat(R"EOF( +name: envoy.filters.http.istio_stats +typed_config: + "@type": type.googleapis.com/stats.PluginConfig +)EOF", + stats_body)); + } + + const auto& extras = is_client ? client_extra_filters_ : server_extra_filters_; + for (const auto& extra : std::ranges::reverse_view(extras)) { + helper.prependFilter(extra); + } + + helper.prependFilter(peerMetadataConfig(is_client)); + } + + // CLIENT: upstream discovery (read peer from response) + optional upstream + // propagation (send our identity on the request). + // SERVER: downstream discovery + // (read peer from request, via istio_headers or baggage) + optional downstream + // propagation (send our identity on the response). + std::string peerMetadataConfig(bool is_client) const { + io::istio::http::peer_metadata::Config config; + + const auto addDiscoveryMethod = [](auto* methods, absl::string_view mode) -> void { + auto* method = methods->Add(); + if (mode == "istio_headers") { + method->mutable_istio_headers(); + } else if (mode == "baggage") { + method->mutable_baggage(); + } else { + ADD_FAILURE() << "Unsupported peer metadata discovery mode: " << mode; + } + }; + const auto addIstioHeadersDiscovery = [&addDiscoveryMethod](auto* methods) { + addDiscoveryMethod(methods, "istio_headers"); + }; + const auto addIstioHeadersPropagation = [](auto* methods) { + methods->Add()->mutable_istio_headers(); + }; + + if (is_client) { + addIstioHeadersDiscovery(config.mutable_upstream_discovery()); + if (client_propagate_) { + addIstioHeadersPropagation(config.mutable_upstream_propagation()); + } + } else { + addDiscoveryMethod(config.mutable_downstream_discovery(), server_downstream_discovery_); + if (server_upstream_discovery_) { + // Waypoint also reads the destination peer from the upstream (app) response. + addIstioHeadersDiscovery(config.mutable_upstream_discovery()); + } + if (server_propagate_) { + addIstioHeadersPropagation(config.mutable_downstream_propagation()); + } + } + + for (const auto& label : mx_additional_labels_) { + config.add_additional_labels(label); + } + + envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter filter; + filter.set_name("envoy.filters.http.peer_metadata"); + std::ignore = filter.mutable_typed_config()->PackFrom(config); + return MessageUtil::getJsonStringFromMessageOrError(filter); + } + + // Encodes a workload as the value of x-envoy-peer-metadata (base64 of a + // deterministically serialized Struct), the wire format peer_metadata expects. + // Used to have the app FakeUpstream play the upstream (destination) peer. + static std::string peerMetadataHeader(const WorkloadMetadataObject& obj) { + const Protobuf::Struct metadata = Istio::Common::convertWorkloadMetadataToStruct(obj); + const std::string bytes = Istio::Common::serializeToStringDeterministic(metadata); + return Base64::encode(bytes.data(), bytes.size()); + } + + // Finalizes `helper` against `upstream_ports`, writes the bootstrap, and starts + // a server instance into `out`. `listener_name` is registered for lookupPort(). + // Reuses the base helper so listener readiness and port registration are handled. + void startSidecar(ConfigHelper& helper, const std::vector& upstream_ports, + const std::string& listener_name, IntegrationTestServerPtr& out) { + helper.finalize(upstream_ports); + const std::string path = TestEnvironment::writeStringToFileForTest( + absl::StrCat("two_proxy_bootstrap_", listener_name, ".pb"), + TestUtility::getProtobufBinaryStringFromMessage(helper.bootstrap())); + createGeneratedApiTestServer(path, {listener_name}, {false, true, false}, false, out); + } + + // Makes a sidecar's listener (HCM) and its single upstream cluster speak HTTP/2. + static void applyHttp2(ConfigHelper& helper) { + helper.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + hcm.set_codec_type(envoy::extensions::filters::network::http_connection_manager::v3:: + HttpConnectionManager::HTTP2); + }); + helper.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + } + + // Brings up app upstream -> SERVER sidecar -> CLIENT sidecar in order, wiring each + // stage's cluster to the next stage's port. After this returns, `test_server_` is + // the CLIENT sidecar and `server_sidecar_` is the SERVER one. + void initializeTwoProxies() { + // For gRPC: HTTP/2 downstream and on the app FakeUpstream + client cluster. + if (http2_hops_) { + downstream_protocol_ = Http::CodecType::HTTP2; + setUpstreamProtocol(Http::CodecType::HTTP2); + } + + // 1. The app the SERVER sidecar forwards to. + setUpstreamCount(1); + createUpstreams(); + const uint32_t app_port = fake_upstreams_[0]->localAddress()->ip()->port(); + + // 2. SERVER sidecar (INBOUND): [peer_metadata, istio.stats, router], cluster -> app. + // Seed its ConfigHelper from the base's still-pristine bootstrap proto (a + // copy, no YAML re-parse) so the two sidecars share the same starting shape. + server_config_ = std::make_unique(version_, config_helper_.bootstrap()); + setSidecarNode(*server_config_, "server-v1", "server-ns", "server-cluster", "server-svc", "v1", + "server-app", "v1"); + setTrafficDirection(*server_config_, envoy::config::core::v3::INBOUND, "inbound"); + prependSidecarFilters(*server_config_, /*is_client=*/false); + if (http2_hops_) { + applyHttp2(*server_config_); + } + for (const auto& modifier : server_modifiers_) { + server_config_->addConfigModifier(modifier); + } + startSidecar(*server_config_, {app_port}, "inbound", server_sidecar_); + server_inbound_port_ = lookupPort("inbound"); + + // 3. CLIENT sidecar (OUTBOUND): [peer_metadata, istio.stats, router], cluster -> + // SERVER sidecar. This one is `test_server_`, the traffic entry point. + setSidecarNode(config_helper_, "client-v1", "client-ns", "client-cluster", "client-svc", "v1", + "client-app", "v1"); + setTrafficDirection(config_helper_, envoy::config::core::v3::OUTBOUND, "outbound"); + prependSidecarFilters(config_helper_, /*is_client=*/true); + if (http2_hops_) { + applyHttp2(config_helper_); + } + for (const auto& modifier : client_modifiers_) { + config_helper_.addConfigModifier(modifier); + } + startSidecar(config_helper_, {server_inbound_port_}, "outbound", test_server_); + } + + // Drives one downstream request through both sidecars; the app FakeUpstream + // answers with `status` (and any `resp_headers`, e.g. an MX header to play the + // upstream peer). Returns the downstream response. + IntegrationStreamDecoderPtr + runRequest(const std::string& method, const std::string& path, const std::string& authority, + const std::string& status = "200", + const std::vector>& extra_req_headers = {}, + const std::vector>& resp_headers = {}) { + codec_client_ = makeHttpConnection(lookupPort("outbound")); + Http::TestRequestHeaderMapImpl request{ + {":method", method}, {":path", path}, {":scheme", "http"}, {":authority", authority}}; + for (const auto& h : extra_req_headers) { + request.addCopy(h.first, h.second); + } + auto response = codec_client_->makeHeaderOnlyRequest(request); + waitForNextUpstreamRequest(); + Http::TestResponseHeaderMapImpl response_headers{{":status", status}}; + for (const auto& h : resp_headers) { + response_headers.addCopy(h.first, h.second); + } + upstream_request_->encodeHeaders(response_headers, true); + EXPECT_TRUE(response->waitForEndStream()); + codec_client_->close(); + return response; + } + + // The canonical distinct client/server/dest peers used across tests. + static WorkloadMetadataObject clientPeer() { + return WorkloadMetadataObject("client-pod-1", "client-cluster", "client-ns", "client-v1", + "client-svc", "v1", "client-app", "v1", WorkloadType::Pod, + "spiffe://client", "", ""); + } + static WorkloadMetadataObject destPeer() { + return WorkloadMetadataObject("dest-pod-1", "dest-cluster", "dest-ns", "dest-v1", "dest-svc", + "v1", "dest-app", "v1", WorkloadType::Pod, "spiffe://dest", "", + ""); + } + + Stats::CounterSharedPtr clientCounter(absl::string_view metric) { + return istioCounter(test_server_->statStore(), metric); + } + Stats::CounterSharedPtr serverCounter(absl::string_view metric) { + return istioCounter(server_sidecar_->statStore(), metric); + } + + // Finds the single istiocustom counter for `metric` in `store`. + static Stats::CounterSharedPtr istioCounter(Stats::Store& store, absl::string_view metric) { + const std::string extracted = absl::StrCat("istiocustom.", metric); + for (const auto& counter : store.counters()) { + if (counter->tagExtractedName() == extracted) { + return counter; + } + } + return nullptr; + } + + static std::optional tagValue(const Stats::Counter& counter, absl::string_view tag) { + for (const auto& t : counter.tags()) { + if (t.name_ == tag) { + return t.value_; + } + } + return std::nullopt; + } + + // --- Per-side configuration, set by tests before initializeTwoProxies(). --- + // Extra stats.PluginConfig YAML appended to each side's istio.stats filter + // (dimensions, tags_to_remove, definitions, disable_host_header_fallback, + // `reporter: SERVER_GATEWAY`, ...). + std::string client_stats_body_; + std::string server_stats_body_; + // Full istio.stats filter YAML override per side (e.g. an ECDS config_discovery + // filter). When set, replaces the inline PluginConfig for that side. + std::string client_stats_filter_override_; + std::string server_stats_filter_override_; + // When false, that side omits propagation, so the peer falls back to endpoint + // metadata instead of an MX header. + bool client_propagate_{true}; + bool server_propagate_{true}; + // When true, run HTTP/2 end to end (downstream, both sidecar listeners and + // clusters, and the app FakeUpstream) -- required for gRPC. + bool http2_hops_{false}; + // SERVER downstream discovery method: "istio_headers" (default) or "baggage". + std::string server_downstream_discovery_{"istio_headers"}; + // Node LABELS keys to carry as additional peer labels (peer_metadata + // additional_labels), e.g. {"role"} for TestAdditionalLabels. + std::vector mx_additional_labels_; + // When true, the SERVER sidecar also reads the upstream (app response) peer -- + // used to model a waypoint that reads both source and destination peers. + bool server_upstream_discovery_{false}; + // Extra HCM filters prepended ahead of istio.stats on each side. + std::vector client_extra_filters_; + std::vector server_extra_filters_; + // Arbitrary bootstrap modifiers per side (cluster/endpoint istio metadata, ECDS). + std::vector client_modifiers_; + std::vector server_modifiers_; + + std::unique_ptr server_config_; + IntegrationTestServerPtr server_sidecar_; + uint32_t server_inbound_port_{0}; +}; + +} // namespace Envoy diff --git a/contrib/istio/filters/common/test/istio_two_proxy_integration_test.cc b/contrib/istio/filters/common/test/istio_two_proxy_integration_test.cc new file mode 100644 index 0000000000000..953aec1ef18ad --- /dev/null +++ b/contrib/istio/filters/common/test/istio_two_proxy_integration_test.cc @@ -0,0 +1,61 @@ +#include "test/test_common/utility.h" + +#include "contrib/istio/filters/common/test/istio_two_proxy_integration_base.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +INSTANTIATE_TEST_SUITE_P(IpVersions, IstioTwoProxyIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Related upstream e2e: stats_plugin/TestStatsPayload (default case). +// Proof of the fixture: one request through both real sidecars, with the Istio +// metadata-exchange handshake happening over the wire (no harness-injected +// headers). Asserts istio_requests_total on BOTH sidecars with the reporter and +// peer identities each side should see -- the same assertions the istio/proxy +// stats_plugin e2e makes on its two sidecars' admin ports. +TEST_P(IstioTwoProxyIntegrationTest, RequestsTotalOnBothSidecars) { + initializeTwoProxies(); + + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "server-svc.server-ns.svc.cluster.local"}, + }); + // The app upstream (behind the SERVER sidecar) receives the proxied request. + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + codec_client_->close(); + + // CLIENT sidecar (OUTBOUND): reporter=source, source identity is the client node, + // destination identity learned from the SERVER sidecar's response MX header. + auto client_counter = istioCounter(test_server_->statStore(), "istio_requests_total"); + ASSERT_NE(client_counter, nullptr); + EXPECT_EQ(1, client_counter->value()); + EXPECT_EQ("source", tagValue(*client_counter, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client_counter, "source_workload").value_or("")); + EXPECT_EQ("client-ns", tagValue(*client_counter, "source_workload_namespace").value_or("")); + EXPECT_EQ("server-v1", tagValue(*client_counter, "destination_workload").value_or("")); + EXPECT_EQ("server-ns", tagValue(*client_counter, "destination_workload_namespace").value_or("")); + + // SERVER sidecar (INBOUND): reporter=destination, source identity learned from the + // CLIENT sidecar's request MX header, destination identity is the server node. + auto server_counter = istioCounter(server_sidecar_->statStore(), "istio_requests_total"); + ASSERT_NE(server_counter, nullptr); + EXPECT_EQ(1, server_counter->value()); + EXPECT_EQ("destination", tagValue(*server_counter, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server_counter, "source_workload").value_or("")); + EXPECT_EQ("client-ns", tagValue(*server_counter, "source_workload_namespace").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server_counter, "destination_workload").value_or("")); + EXPECT_EQ("server-ns", tagValue(*server_counter, "destination_workload_namespace").value_or("")); +} + +} // namespace +} // namespace Envoy diff --git a/contrib/istio/filters/common/test/metadata_object_test.cc b/contrib/istio/filters/common/test/metadata_object_test.cc index 5b0cb3909a981..913b617500070 100644 --- a/contrib/istio/filters/common/test/metadata_object_test.cc +++ b/contrib/istio/filters/common/test/metadata_object_test.cc @@ -1,10 +1,12 @@ +// NOLINT(namespace-envoy) +#include + #include "envoy/registry/registry.h" #include "contrib/istio/filters/common/source/metadata_object.h" #include "gmock/gmock.h" #include "gtest/gtest.h" -namespace Envoy { namespace Istio { namespace Common { @@ -12,18 +14,21 @@ using Envoy::Protobuf::util::MessageDifferencer; TEST(WorkloadMetadataObjectTest, AsString) { constexpr absl::string_view identity = "spiffe://cluster.local/ns/default/sa/default"; - WorkloadMetadataObject deploy("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", WorkloadType::Deployment, identity, "", ""); + ::Istio::Common::WorkloadMetadataObject deploy( + "pod-foo-1234", "my-cluster", "default", "foo", "foo-service", "v1alpha3", "", "", + ::Istio::Common::WorkloadType::Deployment, identity, "", ""); - WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", WorkloadType::Pod, identity, "", ""); + ::Istio::Common::WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", + "foo-service", "v1alpha3", "", "", + ::Istio::Common::WorkloadType::Pod, identity, "", ""); - WorkloadMetadataObject cronjob("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "foo-app", "v1", WorkloadType::CronJob, identity, "", - ""); + ::Istio::Common::WorkloadMetadataObject cronjob( + "pod-foo-1234", "my-cluster", "default", "foo", "foo-service", "v1alpha3", "foo-app", "v1", + ::Istio::Common::WorkloadType::CronJob, identity, "", ""); - WorkloadMetadataObject job("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", WorkloadType::Job, identity, "", ""); + ::Istio::Common::WorkloadMetadataObject job("pod-foo-1234", "my-cluster", "default", "foo", + "foo-service", "v1alpha3", "", "", + ::Istio::Common::WorkloadType::Job, identity, "", ""); EXPECT_EQ(deploy.serializeAsString(), absl::StrCat("type=deployment,workload=foo,name=pod-foo-1234,cluster=my-cluster,", @@ -44,9 +49,9 @@ TEST(WorkloadMetadataObjectTest, AsString) { } void checkStructConversion(const Envoy::StreamInfo::FilterState::Object& data) { - const auto& obj = dynamic_cast(data); - auto pb = convertWorkloadMetadataToStruct(obj); - auto obj2 = convertStructToWorkloadMetadata(pb); + const auto& obj = dynamic_cast(data); + auto pb = ::Istio::Common::convertWorkloadMetadataToStruct(obj); + auto obj2 = ::Istio::Common::convertStructToWorkloadMetadata(pb); EXPECT_EQ(obj2->serializeAsString(), obj.serializeAsString()); MessageDifferencer::Equals(*(obj2->serializeAsProto()), *(obj.serializeAsProto())); EXPECT_EQ(obj2->hash(), obj.hash()); @@ -54,30 +59,31 @@ void checkStructConversion(const Envoy::StreamInfo::FilterState::Object& data) { TEST(WorkloadMetadataObjectTest, ConversionWithLabels) { constexpr absl::string_view identity = "spiffe://cluster.local/ns/default/sa/default"; - WorkloadMetadataObject deploy("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", WorkloadType::Deployment, identity, "", ""); + ::Istio::Common::WorkloadMetadataObject deploy( + "pod-foo-1234", "my-cluster", "default", "foo", "foo-service", "v1alpha3", "", "", + ::Istio::Common::WorkloadType::Deployment, identity, "", ""); deploy.setLabels({{"label1", "value1"}, {"label2", "value2"}}); - auto pb = convertWorkloadMetadataToStruct(deploy); - auto obj1 = convertStructToWorkloadMetadata(pb, {"label1", "label2"}); + auto pb = ::Istio::Common::convertWorkloadMetadataToStruct(deploy); + auto obj1 = ::Istio::Common::convertStructToWorkloadMetadata(pb, {"label1", "label2"}); EXPECT_EQ(obj1->getLabels().size(), 2); - auto obj2 = convertStructToWorkloadMetadata(pb, {"label1"}); + auto obj2 = ::Istio::Common::convertStructToWorkloadMetadata(pb, {"label1"}); EXPECT_EQ(obj2->getLabels().size(), 1); absl::flat_hash_set empty; - auto obj3 = convertStructToWorkloadMetadata(pb, empty); + auto obj3 = ::Istio::Common::convertStructToWorkloadMetadata(pb, empty); EXPECT_EQ(obj3->getLabels().size(), 0); } TEST(WorkloadMetadataObjectTest, Conversion) { { constexpr absl::string_view identity = "spiffe://cluster.local/ns/default/sa/default"; - const auto r = convertBaggageToWorkloadMetadata( + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( "k8s.deployment.name=foo,k8s.cluster.name=my-cluster," "k8s.namespace.name=default,service.name=foo-service,service.version=v1alpha3,app.name=foo-" "app,app.version=latest", identity); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1alpha3"); - EXPECT_EQ(absl::get(r->getField("type")), DeploymentSuffix); + EXPECT_EQ(absl::get(r->getField("type")), ::Istio::Common::DeploymentSuffix); EXPECT_EQ(absl::get(r->getField("workload")), "foo"); EXPECT_EQ(absl::get(r->getField("name")), ""); EXPECT_EQ(absl::get(r->getField("namespace")), "default"); @@ -89,12 +95,12 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata( + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( "k8s.pod.name=foo-pod-435,k8s.cluster.name=my-cluster,k8s.namespace.name=" "test,k8s.instance.name=foo-instance-435,service.name=foo-service,service.version=v1beta2"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1beta2"); - EXPECT_EQ(absl::get(r->getField("type")), PodSuffix); + EXPECT_EQ(absl::get(r->getField("type")), ::Istio::Common::PodSuffix); EXPECT_EQ(absl::get(r->getField("workload")), "foo-pod-435"); EXPECT_EQ(absl::get(r->getField("name")), "foo-instance-435"); EXPECT_EQ(absl::get(r->getField("namespace")), "test"); @@ -105,12 +111,12 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata( + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( "k8s.job.name=foo-job-435,k8s.cluster.name=my-cluster,k8s.namespace.name=" "test,k8s.instance.name=foo-instance-435,service.name=foo-service,service.version=v1beta4"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1beta4"); - EXPECT_EQ(absl::get(r->getField("type")), JobSuffix); + EXPECT_EQ(absl::get(r->getField("type")), ::Istio::Common::JobSuffix); EXPECT_EQ(absl::get(r->getField("workload")), "foo-job-435"); EXPECT_EQ(absl::get(r->getField("name")), "foo-instance-435"); EXPECT_EQ(absl::get(r->getField("namespace")), "test"); @@ -121,12 +127,12 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata( + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( "k8s.cronjob.name=foo-cronjob,k8s.cluster.name=my-cluster," "k8s.namespace.name=test,service.name=foo-service,service.version=v1beta4"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1beta4"); - EXPECT_EQ(absl::get(r->getField("type")), CronJobSuffix); + EXPECT_EQ(absl::get(r->getField("type")), ::Istio::Common::CronJobSuffix); EXPECT_EQ(absl::get(r->getField("workload")), "foo-cronjob"); EXPECT_EQ(absl::get(r->getField("name")), ""); EXPECT_EQ(absl::get(r->getField("namespace")), "test"); @@ -137,12 +143,12 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = - convertBaggageToWorkloadMetadata("k8s.deployment.name=foo,k8s.namespace.name=default," - "service.name=foo-service,service.version=v1alpha3"); + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( + "k8s.deployment.name=foo,k8s.namespace.name=default," + "service.name=foo-service,service.version=v1alpha3"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1alpha3"); - EXPECT_EQ(absl::get(r->getField("type")), DeploymentSuffix); + EXPECT_EQ(absl::get(r->getField("type")), ::Istio::Common::DeploymentSuffix); EXPECT_EQ(absl::get(r->getField("workload")), "foo"); EXPECT_EQ(absl::get(r->getField("namespace")), "default"); EXPECT_EQ(absl::get(r->getField("cluster")), ""); @@ -152,8 +158,8 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = - convertBaggageToWorkloadMetadata("service.name=foo-service,service.version=v1alpha3"); + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( + "service.name=foo-service,service.version=v1alpha3"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1alpha3"); EXPECT_EQ(absl::get(r->getField("app")), "foo-service"); @@ -162,7 +168,8 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata("app.name=foo-app,app.version=latest"); + const auto r = + ::Istio::Common::convertBaggageToWorkloadMetadata("app.name=foo-app,app.version=latest"); EXPECT_EQ(absl::get(r->getField("service")), "foo-app"); EXPECT_EQ(absl::get(r->getField("revision")), "latest"); EXPECT_EQ(absl::get(r->getField("app")), "foo-app"); @@ -171,7 +178,7 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata( + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata( "service.name=foo-service,service.version=v1alpha3,app.name=foo-app,app.version=latest"); EXPECT_EQ(absl::get(r->getField("service")), "foo-service"); EXPECT_EQ(absl::get(r->getField("revision")), "v1alpha3"); @@ -181,25 +188,25 @@ TEST(WorkloadMetadataObjectTest, Conversion) { } { - const auto r = convertBaggageToWorkloadMetadata("k8s.namespace.name=default"); + const auto r = ::Istio::Common::convertBaggageToWorkloadMetadata("k8s.namespace.name=default"); EXPECT_EQ(absl::get(r->getField("namespace")), "default"); checkStructConversion(*r); } } TEST(WorkloadMetadataObjectTest, ConvertFromEmpty) { - Protobuf::Struct node; - auto obj = convertStructToWorkloadMetadata(node); + Envoy::Protobuf::Struct node; + auto obj = ::Istio::Common::convertStructToWorkloadMetadata(node); EXPECT_EQ(obj->serializeAsString(), ""); checkStructConversion(*obj); } TEST(WorkloadMetadataObjectTest, ConvertFromEndpointMetadata) { - EXPECT_EQ(absl::nullopt, convertEndpointMetadata("")); - EXPECT_EQ(absl::nullopt, convertEndpointMetadata("a;b")); - EXPECT_EQ(absl::nullopt, convertEndpointMetadata("a;;;b")); - EXPECT_EQ(absl::nullopt, convertEndpointMetadata("a;b;c;d")); - auto obj = convertEndpointMetadata("foo-pod;default;foo-service;v1;my-cluster"); + EXPECT_EQ(std::nullopt, ::Istio::Common::convertEndpointMetadata("")); + EXPECT_EQ(std::nullopt, ::Istio::Common::convertEndpointMetadata("a;b")); + EXPECT_EQ(std::nullopt, ::Istio::Common::convertEndpointMetadata("a;;;b")); + EXPECT_EQ(std::nullopt, ::Istio::Common::convertEndpointMetadata("a;b;c;d")); + auto obj = ::Istio::Common::convertEndpointMetadata("foo-pod;default;foo-service;v1;my-cluster"); ASSERT_TRUE(obj.has_value()); EXPECT_EQ(obj->serializeAsString(), "workload=foo-pod,cluster=my-cluster," "namespace=default,service=foo-service,revision=v1"); @@ -207,4 +214,3 @@ TEST(WorkloadMetadataObjectTest, ConvertFromEndpointMetadata) { } // namespace Common } // namespace Istio -} // namespace Envoy diff --git a/contrib/istio/filters/http/alpn/source/alpn_filter.cc b/contrib/istio/filters/http/alpn/source/alpn_filter.cc index 9358aeff340ee..a73f8862d7727 100644 --- a/contrib/istio/filters/http/alpn/source/alpn_filter.cc +++ b/contrib/istio/filters/http/alpn/source/alpn_filter.cc @@ -41,7 +41,7 @@ Http::Protocol AlpnFilterConfig::getHttpProtocol( } Http::FilterHeadersStatus AlpnFilter::decodeHeaders(Http::RequestHeaderMap&, bool) { - const auto route = decoder_callbacks_->route(); + Router::RouteConstSharedPtr route = decoder_callbacks_->routeSharedPtr(); const Router::RouteEntry* route_entry; if (!route || !(route_entry = route->routeEntry())) { ENVOY_LOG(debug, "cannot find route entry"); @@ -78,8 +78,7 @@ Http::FilterHeadersStatus AlpnFilter::decodeHeaders(Http::RequestHeaderMap&, boo ENVOY_LOG(debug, "override with {} ALPNs", alpn_override.size()); decoder_callbacks_->streamInfo().filterState()->setData( Network::ApplicationProtocols::key(), - std::make_unique(alpn_override), - Envoy::StreamInfo::FilterState::StateType::ReadOnly); + std::make_unique(alpn_override)); } else { ENVOY_LOG(debug, "ALPN override is empty"); } diff --git a/contrib/istio/filters/http/alpn/test/alpn_test.cc b/contrib/istio/filters/http/alpn/test/alpn_test.cc index 8db807054af27..004cca3d29891 100644 --- a/contrib/istio/filters/http/alpn/test/alpn_test.cc +++ b/contrib/istio/filters/http/alpn/test/alpn_test.cc @@ -1,3 +1,5 @@ +#include + #include "source/common/network/application_protocol.h" #include "test/mocks/http/mocks.h" @@ -72,7 +74,7 @@ TEST_F(AlpnFilterTest, OverrideAlpnUseDownstreamProtocol) { ON_CALL(cluster_manager_, getThreadLocalCluster(_)).WillByDefault(Return(fake_cluster_.get())); ON_CALL(*fake_cluster_, info()).WillByDefault(Return(cluster_info_)); ON_CALL(*cluster_info_, upstreamHttpProtocol(_)) - .WillByDefault([](absl::optional protocol) -> std::vector { + .WillByDefault([](std::optional protocol) -> std::vector { return {protocol.value()}; }); @@ -106,7 +108,7 @@ TEST_F(AlpnFilterTest, OverrideAlpn) { ON_CALL(cluster_manager_, getThreadLocalCluster(_)).WillByDefault(Return(fake_cluster_.get())); ON_CALL(*fake_cluster_, info()).WillByDefault(Return(cluster_info_)); ON_CALL(*cluster_info_, upstreamHttpProtocol(_)) - .WillByDefault([](absl::optional) -> std::vector { + .WillByDefault([](std::optional) -> std::vector { return {Http::Protocol::Http2}; }); @@ -139,7 +141,7 @@ TEST_F(AlpnFilterTest, EmptyOverrideAlpn) { ON_CALL(cluster_manager_, getThreadLocalCluster(_)).WillByDefault(Return(fake_cluster_.get())); ON_CALL(*fake_cluster_, info()).WillByDefault(Return(cluster_info_)); ON_CALL(*cluster_info_, upstreamHttpProtocol(_)) - .WillByDefault([](absl::optional) -> std::vector { + .WillByDefault([](std::optional) -> std::vector { return {Http::Protocol::Http2}; }); diff --git a/contrib/istio/filters/http/istio_stats/source/istio_stats.cc b/contrib/istio/filters/http/istio_stats/source/istio_stats.cc index 00140d8addd4b..c7e872eb86cef 100644 --- a/contrib/istio/filters/http/istio_stats/source/istio_stats.cc +++ b/contrib/istio/filters/http/istio_stats/source/istio_stats.cc @@ -15,6 +15,7 @@ #include "contrib/istio/filters/http/istio_stats/source/istio_stats.h" #include +#include #include "envoy/registry/registry.h" #include "envoy/router/string_accessor.h" @@ -56,7 +57,7 @@ namespace { constexpr absl::string_view NamespaceKey = "/ns/"; -absl::optional getNamespace(absl::string_view principal) { +std::optional getNamespace(absl::string_view principal) { // The namespace is a substring in principal with format: // "/ns//sa/". '/' is not allowed to // appear in actual content except as delimiter between tokens. @@ -89,7 +90,7 @@ absl::string_view extractMapString(const Protobuf::Struct& metadata, const std:: return extractString(it->second.struct_value(), key); } -absl::optional +std::optional extractEndpointMetadata(const StreamInfo::StreamInfo& info) { auto upstream_info = info.upstreamInfo(); auto upstream_host = upstream_info ? upstream_info->upstreamHost() : nullptr; @@ -126,7 +127,7 @@ bool peerInfoRead(Reporter reporter, const StreamInfo::FilterState& filter_state filter_state.hasDataWithName(Istio::Common::NoPeer); } -absl::optional +std::optional peerInfo(Reporter reporter, const StreamInfo::FilterState& filter_state) { const auto& cel_state_key = reporter == Reporter::ServerSidecar || reporter == Reporter::ServerGateway @@ -393,7 +394,7 @@ struct MetricOverrides : public Logger::Loggable { // Initial transformation: metrics dropped. absl::flat_hash_set drop_; // Second transformation: tags changed. - using TagOverrides = absl::flat_hash_map>; + using TagOverrides = absl::flat_hash_map>; absl::flat_hash_map tag_overrides_; // Third transformation: tags added. using TagAdditions = std::vector>; @@ -429,7 +430,7 @@ struct MetricOverrides : public Logger::Loggable { } return out; } - absl::optional getOrCreateExpression(const std::string& expr, bool int_expr) { + std::optional getOrCreateExpression(const std::string& expr, bool int_expr) { const auto& it = expression_ids_.find(expr); if (it != expression_ids_.end()) { return {it->second}; @@ -487,7 +488,7 @@ struct Config : public Logger::Loggable { reporter_ = Reporter::ClientSidecar; switch (proto_config.reporter()) { case stats::Reporter::UNSPECIFIED: - switch (factory_context.listenerInfo().direction()) { + switch (factory_context.direction()) { case envoy::config::core::v3::TrafficDirection::INBOUND: reporter_ = Reporter::ServerSidecar; break; @@ -977,7 +978,7 @@ class IstioStatsFilter : public Http::PassThroughFilter, void populatePeerInfo(const StreamInfo::StreamInfo& info, const StreamInfo::FilterState& filter_state) { // Compute peer info with client-side fallbacks. - absl::optional peer; + std::optional peer; auto object = peerInfo(config_->reporter(), filter_state); if (object) { peer.emplace(object.value()); @@ -1072,7 +1073,7 @@ class IstioStatsFilter : public Http::PassThroughFilter, case Reporter::ClientSidecar: { const Ssl::ConnectionInfoConstSharedPtr ssl_info = info.upstreamInfo() ? info.upstreamInfo()->upstreamSslConnection() : nullptr; - absl::optional endpoint_peer; + std::optional endpoint_peer; if (ssl_info && !ssl_info->uriSanPeerCertificate().empty()) { peer_san = ssl_info->uriSanPeerCertificate()[0]; } @@ -1131,7 +1132,7 @@ class IstioStatsFilter : public Http::PassThroughFilter, : context_.unknown_}); switch (config_->reporter()) { case Reporter::ServerGateway: { - absl::optional endpoint_peer; + std::optional endpoint_peer; auto endpoint_object = peerInfo(Reporter::ClientSidecar, filter_state); if (endpoint_object) { endpoint_peer.emplace(endpoint_object.value()); @@ -1260,7 +1261,7 @@ class IstioStatsFilter : public Http::PassThroughFilter, bool peer_read_{false}; uint64_t bytes_sent_{0}; uint64_t bytes_received_{0}; - absl::optional mutual_tls_; + std::optional mutual_tls_; bool is_grpc_{false}; uint64_t request_message_count_{0}; uint64_t response_message_count_{0}; diff --git a/contrib/istio/filters/http/istio_stats/test/integration/BUILD b/contrib/istio/filters/http/istio_stats/test/integration/BUILD new file mode 100644 index 0000000000000..37c249c3835ca --- /dev/null +++ b/contrib/istio/filters/http/istio_stats/test/integration/BUILD @@ -0,0 +1,32 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +# Integration suite for the Istio `istio.stats` HTTP filter, migrating the +# istio/proxy `stats_plugin` two-sidecar e2e onto the two-Envoy fixture: every +# case runs a real client sidecar and a real server sidecar that perform the +# genuine metadata-exchange handshake over the wire, with assertions on each +# sidecar's stat store (core HTTP stats, the istio.stats CEL engine, the +# filter-state dimension contract, waypoint/baggage, gRPC, and ECDS delivery). +envoy_cc_test( + name = "istio_stats_integration_test", + srcs = ["istio_stats_integration_test.cc"], + deps = [ + "//contrib/istio/filters/common/test:istio_two_proxy_integration_lib", + "//contrib/istio/filters/http/istio_stats/source:istio_stats", + "//contrib/istio/filters/http/peer_metadata/source:config", + "//source/common/buffer:buffer_lib", + "//source/common/grpc:common_lib", + "//source/extensions/filters/http/grpc_stats:config", + "//source/extensions/filters/http/rbac:config", + "//source/extensions/filters/http/router:config", + "//source/extensions/filters/http/set_filter_state:config", + "//test/test_common:utility_lib", + ], +) diff --git a/contrib/istio/filters/http/istio_stats/test/integration/istio_stats_integration_test.cc b/contrib/istio/filters/http/istio_stats/test/integration/istio_stats_integration_test.cc new file mode 100644 index 0000000000000..4260ce26b90cd --- /dev/null +++ b/contrib/istio/filters/http/istio_stats/test/integration/istio_stats_integration_test.cc @@ -0,0 +1,1009 @@ +#include +#include +#include +#include + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/grpc/common.h" + +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "contrib/istio/filters/common/test/istio_two_proxy_integration_base.h" +#include "gtest/gtest.h" + +// Integration suite for the Istio `istio.stats` HTTP filter, migrating the +// istio/proxy `stats_plugin` e2e. Every case runs on the two-Envoy fixture +// (`IstioTwoProxyIntegrationTest`): a real CLIENT sidecar (OUTBOUND, +// reporter=source) and a real SERVER sidecar (INBOUND, reporter=destination) +// that perform the genuine client<->server metadata-exchange handshake over the +// wire -- the sidecars exchange their own node identities, not harness-injected +// headers (some cases additionally have the app upstream return an MX response +// header to play the destination peer). Assertions are made on each sidecar's own +// stat store, exactly as the Go e2e asserts golden metrics on both sidecars. +namespace Envoy { +namespace { + +INSTANTIATE_TEST_SUITE_P(IpVersions, IstioTwoProxyIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Related upstream e2e: stats_plugin/TestStatsPayload (default case). +// TestStatsPayload (Default), both sidecars. The client learns the destination +// identity from the server's response MX header and the server learns the source +// identity from the client's request MX header -- the real handshake, asserted on +// both stat stores. +TEST_P(IstioTwoProxyIntegrationTest, RequestsTotalBothSidecars) { + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ(1, client->value()); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("200", tagValue(*client, "response_code").value_or("")); + EXPECT_EQ("http", tagValue(*client, "request_protocol").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*client, "destination_workload").value_or("")); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("200", tagValue(*server, "response_code").value_or("")); + EXPECT_EQ("http", tagValue(*server, "request_protocol").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("client-ns", tagValue(*server, "source_workload_namespace").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server, "destination_workload").value_or("")); + EXPECT_EQ("server-ns", tagValue(*server, "destination_workload_namespace").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (custom-dimension case). +// Custom dimension via a CEL expression over a request attribute, on the server. +TEST_P(IstioTwoProxyIntegrationTest, CustomCelDimension) { + server_stats_body_ = R"EOF( metrics: + - dimensions: + request_operation: "request.method" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("GET", tagValue(*server, "request_operation").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (CEL-expression case). +// istio.stats CEL engine: string concatenation, equality, and a ternary. +TEST_P(IstioTwoProxyIntegrationTest, CelEngineDimensions) { + server_stats_body_ = R"EOF( metrics: + - dimensions: + request_operation: "request.method + ' ' + request.url_path" + path_is_root: "request.url_path == '/' ? 'yes' : 'no'" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/api/orders", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("GET /api/orders", tagValue(*server, "request_operation").value_or("")); + EXPECT_EQ("no", tagValue(*server, "path_is_root").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestAdditionalLabels. +// TestAdditionalLabels: a node LABELS key (`role`) is carried as an additional peer +// label via peer_metadata `additional_labels`, and surfaced as a stats dimension on +// BOTH sidecars (upstream asserts the labeled metric on both admin ports, not one +// direction). The client propagates its `role` on the request and the server reads it +// into the downstream peer; symmetrically the server propagates its `role` on the +// response and the client reads it into the upstream peer. +TEST_P(IstioTwoProxyIntegrationTest, AdditionalLabels) { + mx_additional_labels_ = {"role"}; + // Each node carries a distinct `role` label to propagate. + client_modifiers_.push_back([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& labels = *(*bootstrap.mutable_node()->mutable_metadata()->mutable_fields())["LABELS"] + .mutable_struct_value() + ->mutable_fields(); + labels["role"].set_string_value("ingress"); + }); + server_modifiers_.push_back([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& labels = *(*bootstrap.mutable_node()->mutable_metadata()->mutable_fields())["LABELS"] + .mutable_struct_value() + ->mutable_fields(); + labels["role"].set_string_value("gateway"); + }); + // SERVER reads the source (client) role from the downstream peer; CLIENT reads the + // destination (server) role from the upstream peer learned via the response MX. + server_stats_body_ = R"EOF( metrics: + - dimensions: + role: "filter_state.downstream_peer.labels['role']" +)EOF"; + client_stats_body_ = R"EOF( metrics: + - dimensions: + role: "filter_state.upstream_peer.labels['role']" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + // Forward direction: the server learned the client's `role` from the request MX. + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("ingress", tagValue(*server, "role").value_or("")); + + // Reverse direction: the client learned the server's `role` from the response MX. + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("gateway", tagValue(*client, "role").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestAttributeGen. +// Filter-state dimension (migrates TestAttributeGen): a native set_filter_state +// filter mocks the AttributeGen plugin's classified output under the wasm. +// key namespace, and istio.stats reads it via filter_state[...] as a dimension. +TEST_P(IstioTwoProxyIntegrationTest, RequestOperationFromFilterState) { + server_extra_filters_.push_back(R"EOF( +name: envoy.filters.http.set_filter_state +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.set_filter_state.v3.Config + on_request_headers: + - object_key: wasm.istio_operationId + factory_key: envoy.string + format_string: + text_format_source: + inline_string: "GetMethod" +)EOF"); + server_stats_body_ = R"EOF( metrics: + - dimensions: + request_operation: "filter_state['wasm.istio_operationId']" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("GetMethod", tagValue(*server, "request_operation").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (host-header fallback case). +// Host-header fallback (default): destination_service is the request authority. +TEST_P(IstioTwoProxyIntegrationTest, DestinationServiceFromHostHeader) { + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("server-svc.server-ns.svc.cluster.local", + tagValue(*server, "destination_service").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (disable_host_header_fallback case). +// Host-header fallback disabled: destination_service falls back to the canonical +// service name regardless of the request authority. +TEST_P(IstioTwoProxyIntegrationTest, DisableHostHeaderFallback) { + server_stats_body_ = R"EOF( disable_host_header_fallback: true +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "ignored.example.com"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("server-svc", tagValue(*server, "destination_service").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (tags_to_remove case). +// tags_to_remove drops a standard dimension. +TEST_P(IstioTwoProxyIntegrationTest, TagsToRemove) { + server_stats_body_ = R"EOF( metrics: + - tags_to_remove: + - request_protocol +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_FALSE(tagValue(*server, "request_protocol").has_value()); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsParserRegression. +// A custom metric `definitions` entry creates a new istio_ counter +// (also exercises the customized-config parser path, TestStatsParserRegression). +TEST_P(IstioTwoProxyIntegrationTest, CustomMetricDefinition) { + server_stats_body_ = R"EOF( definitions: + - name: my_count + type: COUNTER + value: "1" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_my_count"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (UseHostHeader, client side). +// On the CLIENT (source reporter) destination_service falls back to the request +// authority (Host header) when the upstream cluster carries no istio `services` +// metadata -- the source-reporter counterpart of DestinationServiceFromHostHeader. +TEST_P(IstioTwoProxyIntegrationTest, ClientDestinationServiceFromHostHeader) { + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("server-svc.server-ns.svc.cluster.local", + tagValue(*client, "destination_service").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (DisableHostHeader, client side). +// With host-header fallback disabled on the CLIENT and no upstream cluster +// `services` metadata, the source reporter has no destination service to report, +// so destination_service is "unknown" -- the source-reporter counterpart of +// DisableHostHeaderFallback. +TEST_P(IstioTwoProxyIntegrationTest, ClientDisableHostHeaderFallback) { + client_stats_body_ = R"EOF( disable_host_header_fallback: true +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "ignored.example.com"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("unknown", tagValue(*client, "destination_service").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (Customized, client side). +// Customized config on the CLIENT: a CEL custom dimension on istio_requests_total +// plus a custom metric `definitions` entry, both surfaced on the source reporter -- +// the source-reporter counterpart of CustomCelDimension + CustomMetricDefinition. +TEST_P(IstioTwoProxyIntegrationTest, ClientCustomConfig) { + client_stats_body_ = R"EOF( metrics: + - dimensions: + request_operation: "request.method" + definitions: + - name: my_count + type: COUNTER + value: "1" +)EOF"; + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("GET", tagValue(*client, "request_operation").value_or("")); + + auto custom = clientCounter("istio_my_count"); + ASSERT_NE(custom, nullptr); + EXPECT_EQ(1, custom->value()); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (upstream-403 response-code case). +// A non-2xx *upstream* response is reflected in response_code on both sidecars (the +// normal upstream-response encode path). The local-reply path is covered separately by +// RbacLocalReply403 below. +TEST_P(IstioTwoProxyIntegrationTest, ResponseCode403) { + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local", /*status=*/"403"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("403", tagValue(*client, "response_code").value_or("")); + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("403", tagValue(*server, "response_code").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStats403Failure. +// Migrates TestStats403Failure: a server-side RBAC filter DENIES the request, producing +// a *local reply* (403 "RBAC: access denied") -- the request never reaches the app. This +// exercises the local-reply stats path (distinct from an upstream-origin 403): istio.stats +// runs on the encode path of the locally-generated response and must still record +// istio_requests_total with response_code=403 and the source peer discovered by the +// peer_metadata filter ahead of RBAC. The RBAC filter sits between peer_metadata and +// istio.stats, matching the upstream [mx, rbac, stats] chain. +TEST_P(IstioTwoProxyIntegrationTest, RbacLocalReply403) { + server_extra_filters_.push_back(R"EOF( +name: envoy.filters.http.rbac +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: DENY + policies: + deny-all: + permissions: + - any: true + principals: + - any: true +)EOF"); + initializeTwoProxies(); + + // Drive the request directly -- RBAC denies at the server, so the app upstream is never + // hit (runRequest's waitForNextUpstreamRequest would hang). + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "server-svc.server-ns.svc.cluster.local"}}); + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_EQ("403", response->headers().getStatusValue()); + codec_client_->close(); + + // The server recorded the locally-generated 403 with the source identity discovered + // before RBAC ran -- the local-reply path TestStats403Failure exists to cover. + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("403", tagValue(*server, "response_code").value_or("")); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + + // The client saw the 403 as its upstream response. + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("403", tagValue(*client, "response_code").value_or("")); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsPayload (cluster services metadata case). +// Istio `services` metadata on the destination cluster takes precedence over the +// Host header for destination_service*. +TEST_P(IstioTwoProxyIntegrationTest, DestinationServiceFromClusterMetadata) { + server_modifiers_.push_back([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + Protobuf::Struct istio; + auto* services = (*istio.mutable_fields())["services"].mutable_list_value(); + auto* svc = services->add_values()->mutable_struct_value(); + (*svc->mutable_fields())["host"].set_string_value("ratings.bookinfo.svc.cluster.local"); + (*svc->mutable_fields())["name"].set_string_value("ratings"); + (*svc->mutable_fields())["namespace"].set_string_value("bookinfo"); + (*cluster->mutable_metadata()->mutable_filter_metadata())["istio"] = istio; + }); + initializeTwoProxies(); + runRequest("GET", "/", "different.example.com"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("ratings.bookinfo.svc.cluster.local", + tagValue(*server, "destination_service").value_or("")); + EXPECT_EQ("ratings", tagValue(*server, "destination_service_name").value_or("")); + // destination_service/name come from the cluster `services` metadata (winning over + // the "different.example.com" Host header) -- the precedence this test exists for. + // destination_service_namespace tracks the destination workload (node) namespace. + EXPECT_EQ("server-ns", tagValue(*server, "destination_service_namespace").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsDestinationServiceNamespacePrecedence. +// Migrates TestStatsDestinationServiceNamespacePrecedence: on the CLIENT (source +// reporter), destination_service_namespace is taken from the upstream cluster's istio +// `services[0].namespace` metadata, which takes precedence over the destination peer's +// own workload namespace (istio_stats.cc ClientSidecar branch). Here the service +// namespace ("server") is deliberately different from the peer/workload namespace +// ("default") so the precedence is observable. +TEST_P(IstioTwoProxyIntegrationTest, DestinationServiceNamespacePrecedence) { + // The destination peer is derived from the upstream endpoint metadata (not MX + // propagation), giving it a workload namespace distinct from the service namespace. + server_propagate_ = false; + client_modifiers_.push_back([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + // Upstream cluster service metadata: service "server" in namespace "server". + Protobuf::Struct istio; + auto* services = (*istio.mutable_fields())["services"].mutable_list_value(); + auto* svc = services->add_values()->mutable_struct_value(); + (*svc->mutable_fields())["host"].set_string_value("server.default.svc.cluster.local"); + (*svc->mutable_fields())["name"].set_string_value("server"); + (*svc->mutable_fields())["namespace"].set_string_value("server"); + (*cluster->mutable_metadata()->mutable_filter_metadata())["istio"] = istio; + // Destination peer (endpoint) workload lives in namespace "default". + auto* lb = cluster->mutable_load_assignment()->mutable_endpoints(0)->mutable_lb_endpoints(0); + Protobuf::Struct ep; + (*ep.mutable_fields())["workload"].set_string_value( + "ratings-v1;default;ratings;v1;server-cluster"); + (*lb->mutable_metadata()->mutable_filter_metadata())["istio"] = ep; + }); + initializeTwoProxies(); + runRequest("GET", "/", "server.default.svc.cluster.local"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("ratings-v1", tagValue(*client, "destination_workload").value_or("")); + // The peer/workload namespace is "default". + EXPECT_EQ("default", tagValue(*client, "destination_workload_namespace").value_or("")); + EXPECT_EQ("server.default.svc.cluster.local", + tagValue(*client, "destination_service").value_or("")); + EXPECT_EQ("server", tagValue(*client, "destination_service_name").value_or("")); + // The service namespace ("server") wins over the peer namespace ("default"). + EXPECT_EQ("server", tagValue(*client, "destination_service_namespace").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsEndpointLabels. +// Client-side endpoint metadata fallback: with the server NOT propagating its +// identity, the client derives the destination from the upstream endpoint's +// istio/workload metadata (semicolon-encoded workload;ns;canonical;rev;cluster). +TEST_P(IstioTwoProxyIntegrationTest, ClientEndpointLabels) { + server_propagate_ = false; + client_modifiers_.push_back([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* lb = bootstrap.mutable_static_resources() + ->mutable_clusters(0) + ->mutable_load_assignment() + ->mutable_endpoints(0) + ->mutable_lb_endpoints(0); + Protobuf::Struct istio; + (*istio.mutable_fields())["workload"].set_string_value( + "ratings-v1;bookinfo;ratings;v1;remote-cluster"); + (*lb->mutable_metadata()->mutable_filter_metadata())["istio"] = istio; + }); + initializeTwoProxies(); + runRequest("GET", "/", "ratings"); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("ratings-v1", tagValue(*client, "destination_workload").value_or("")); + EXPECT_EQ("bookinfo", tagValue(*client, "destination_workload_namespace").value_or("")); + EXPECT_EQ("ratings", tagValue(*client, "destination_canonical_service").value_or("")); + // Endpoint metadata supplies workload/namespace/canonical but NOT app/version, so + // those resolve to "unknown" -- the signal TestStatsEndpointLabels asserts. + EXPECT_EQ("unknown", tagValue(*client, "destination_app").value_or("")); + EXPECT_EQ("unknown", tagValue(*client, "destination_version").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsServerWaypointProxy. +// Waypoint (TestStatsServerWaypointProxy): the SERVER sidecar runs as a waypoint +// (reporter=SERVER_GATEWAY), reading the source from the downstream (client) MX +// peer and the destination from the upstream (app) MX peer. +TEST_P(IstioTwoProxyIntegrationTest, WaypointReporter) { + server_stats_body_ = R"EOF( reporter: SERVER_GATEWAY +)EOF"; + server_upstream_discovery_ = true; + initializeTwoProxies(); + runRequest("GET", "/", "dest-svc.dest-ns.svc.cluster.local", /*status=*/"200", + /*extra_req_headers=*/{}, + /*resp_headers=*/ + {{"x-envoy-peer-metadata-id", "dest-id"}, + {"x-envoy-peer-metadata", peerMetadataHeader(destPeer())}}); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("client-ns", tagValue(*server, "source_workload_namespace").value_or("")); + EXPECT_EQ("dest-v1", tagValue(*server, "destination_workload").value_or("")); + EXPECT_EQ("dest-ns", tagValue(*server, "destination_workload_namespace").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsWithBaggageWaypointProxy. +// Waypoint with a baggage-discovered source (TestStatsWithBaggageWaypointProxy): +// the source identity arrives in the W3C `baggage` request header (passed through +// the client sidecar), the destination from the upstream (app) MX peer. +TEST_P(IstioTwoProxyIntegrationTest, WaypointBaggagePeerDiscovery) { + server_stats_body_ = R"EOF( reporter: SERVER_GATEWAY +)EOF"; + server_downstream_discovery_ = "baggage"; + server_upstream_discovery_ = true; + initializeTwoProxies(); + runRequest("GET", "/", "dest-svc.dest-ns.svc.cluster.local", /*status=*/"200", + /*extra_req_headers=*/{{"baggage", clientPeer().baggage()}}, + /*resp_headers=*/ + {{"x-envoy-peer-metadata-id", "dest-id"}, + {"x-envoy-peer-metadata", peerMetadataHeader(destPeer())}}); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_EQ("waypoint", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("client-app", tagValue(*server, "source_app").value_or("")); + EXPECT_EQ("dest-v1", tagValue(*server, "destination_workload").value_or("")); +} + +// Builds one length-prefixed gRPC frame carrying `payload`. +Buffer::OwnedImpl grpcFrame(absl::string_view payload) { + Buffer::OwnedImpl frame(payload.data(), payload.size()); + Grpc::Common::prependGrpcFrameHeader(frame); + return frame; +} + +Http::TestRequestHeaderMapImpl grpcRequestHeaders() { + return Http::TestRequestHeaderMapImpl{ + {":method", "POST"}, {":path", "/example.Service/Method"}, + {":scheme", "http"}, {":authority", "server-svc.server-ns.svc.cluster.local"}, + {"te", "trailers"}, {"content-type", "application/grpc"}}; +} + +const std::string kGrpcStatsFilter = R"EOF( +name: envoy.filters.http.grpc_stats +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig + emit_filter_state: true +)EOF"; + +// Related upstream e2e: stats_plugin/TestStatsGrpc. +// gRPC is detected by content-type (request_protocol=grpc) and grpc_response_status +// comes from the grpc-status trailer -- asserted on both the source and destination +// sidecar. +TEST_P(IstioTwoProxyIntegrationTest, GrpcRequestBothSidecars) { + http2_hops_ = true; + server_extra_filters_.push_back(kGrpcStatsFilter); + client_extra_filters_.push_back(kGrpcStatsFilter); + initializeTwoProxies(); + + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto encoder_decoder = codec_client_->startRequest(grpcRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + Buffer::OwnedImpl request_message = grpcFrame("request-body"); + codec_client_->sendData(*request_encoder_, request_message, /*end_stream=*/true); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}, {"content-type", "application/grpc"}}, + false); + // Non-OK gRPC status (7 = PermissionDenied), as TestStatsGrpc asserts. + upstream_request_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "7"}}); + ASSERT_TRUE(response->waitForEndStream()); + codec_client_->close(); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("grpc", tagValue(*server, "request_protocol").value_or("")); + EXPECT_EQ("7", tagValue(*server, "grpc_response_status").value_or("")); + + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("grpc", tagValue(*client, "request_protocol").value_or("")); + EXPECT_EQ("7", tagValue(*client, "grpc_response_status").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsGrpcStream (unary-counting portion). +// Unary gRPC: one request message, one response message; message counters = 1/1 on +// both sidecars. +TEST_P(IstioTwoProxyIntegrationTest, UnaryGrpcBothSidecars) { + http2_hops_ = true; + server_extra_filters_.push_back(kGrpcStatsFilter); + client_extra_filters_.push_back(kGrpcStatsFilter); + initializeTwoProxies(); + + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto encoder_decoder = codec_client_->startRequest(grpcRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + Buffer::OwnedImpl request_message = grpcFrame("request-body"); + codec_client_->sendData(*request_encoder_, request_message, /*end_stream=*/true); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}, {"content-type", "application/grpc"}}, + false); + Buffer::OwnedImpl response_message = grpcFrame("response-body"); + upstream_request_->encodeData(response_message, false); + upstream_request_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); + ASSERT_TRUE(response->waitForEndStream()); + codec_client_->close(); + + for (Stats::Store* store : {&server_sidecar_->statStore(), &test_server_->statStore()}) { + auto req_msgs = istioCounter(*store, "istio_request_messages_total"); + ASSERT_NE(req_msgs, nullptr); + EXPECT_EQ(1, req_msgs->value()); + auto resp_msgs = istioCounter(*store, "istio_response_messages_total"); + ASSERT_NE(resp_msgs, nullptr); + EXPECT_EQ(1, resp_msgs->value()); + } +} + +// Related upstream e2e: stats_plugin/TestStatsGrpcStream. +// Streaming gRPC: several request and response messages; counters accumulate on both +// sidecars. +TEST_P(IstioTwoProxyIntegrationTest, StreamingGrpcBothSidecars) { + http2_hops_ = true; + server_extra_filters_.push_back(kGrpcStatsFilter); + client_extra_filters_.push_back(kGrpcStatsFilter); + initializeTwoProxies(); + + constexpr int kRequestMessages = 3; + constexpr int kResponseMessages = 5; + + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto encoder_decoder = codec_client_->startRequest(grpcRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + for (int i = 0; i < kRequestMessages; ++i) { + Buffer::OwnedImpl message = grpcFrame(absl::StrCat("request-", i)); + codec_client_->sendData(*request_encoder_, message, /*end_stream=*/i == kRequestMessages - 1); + } + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}, {"content-type", "application/grpc"}}, + false); + for (int i = 0; i < kResponseMessages; ++i) { + Buffer::OwnedImpl message = grpcFrame(absl::StrCat("response-", i)); + upstream_request_->encodeData(message, false); + } + upstream_request_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); + ASSERT_TRUE(response->waitForEndStream()); + codec_client_->close(); + + for (Stats::Store* store : {&server_sidecar_->statStore(), &test_server_->statStore()}) { + auto req_msgs = istioCounter(*store, "istio_request_messages_total"); + ASSERT_NE(req_msgs, nullptr); + EXPECT_EQ(kRequestMessages, req_msgs->value()); + auto resp_msgs = istioCounter(*store, "istio_response_messages_total"); + ASSERT_NE(resp_msgs, nullptr); + EXPECT_EQ(kResponseMessages, resp_msgs->value()); + } +} + +// Related upstream e2e: stats_plugin/TestStatsGrpcStream. +// Migrates the mid-stream half of TestStatsGrpcStream: message counters must be reported +// *while the stream is still open*, not only at trailers/close. For gRPC HTTP streams the +// filter arms a periodic report timer (tcp_reporting_duration) whose onReportTimer -> +// reportHelper(false) emits the message-count delta from grpc_stats. With a short interval +// we send a first batch, then -- before closing -- poll the counters until they reflect +// the in-flight messages, then close and assert the final totals. A regression that +// deferred message accounting until trailers would fail the mid-stream assertion. +TEST_P(IstioTwoProxyIntegrationTest, StreamingGrpcMidStreamReporting) { + http2_hops_ = true; + server_extra_filters_.push_back(kGrpcStatsFilter); + client_extra_filters_.push_back(kGrpcStatsFilter); + // Short periodic report interval so the timer fires mid-stream. + server_stats_body_ = R"EOF( tcp_reporting_duration: 0.1s +)EOF"; + client_stats_body_ = R"EOF( tcp_reporting_duration: 0.1s +)EOF"; + initializeTwoProxies(); + + // Polls a store until the istio message counter reaches `want` (the periodic timer runs + // on the sidecar's real-time worker dispatcher, so a real sleep lets it fire). + auto waitForMsgCount = [this](Stats::Store& store, absl::string_view metric, + uint64_t want) -> Stats::CounterSharedPtr { + for (int i = 0; i < 100; ++i) { + auto c = istioCounter(store, metric); + if (c != nullptr && c->value() >= want) { + return c; + } + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(50)); + } + return nullptr; + }; + + constexpr int kFirstRequestMessages = 3; + constexpr int kFirstResponseMessages = 2; + + codec_client_ = makeHttpConnection(lookupPort("outbound")); + auto encoder_decoder = codec_client_->startRequest(grpcRequestHeaders()); + request_encoder_ = &encoder_decoder.first; + auto response = std::move(encoder_decoder.second); + // First batch of request messages, stream left OPEN (end_stream=false). + for (int i = 0; i < kFirstRequestMessages; ++i) { + Buffer::OwnedImpl message = grpcFrame(absl::StrCat("request-", i)); + codec_client_->sendData(*request_encoder_, message, /*end_stream=*/false); + } + + // Wait for the upstream stream's headers only -- the request is deliberately still open, + // so the usual waitForNextUpstreamRequest() (which waits for end_stream) cannot be used. + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForHeadersComplete()); + upstream_request_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}, {"content-type", "application/grpc"}}, + false); + for (int i = 0; i < kFirstResponseMessages; ++i) { + Buffer::OwnedImpl message = grpcFrame(absl::StrCat("response-", i)); + upstream_request_->encodeData(message, false); + } + + // Mid-stream (connection still open): the periodic timer must have published the + // in-flight message counts on the server. + auto mid_req = waitForMsgCount(server_sidecar_->statStore(), "istio_request_messages_total", + kFirstRequestMessages); + ASSERT_NE(mid_req, nullptr); + EXPECT_GE(mid_req->value(), kFirstRequestMessages); + auto mid_resp = waitForMsgCount(server_sidecar_->statStore(), "istio_response_messages_total", + kFirstResponseMessages); + ASSERT_NE(mid_resp, nullptr); + EXPECT_GE(mid_resp->value(), kFirstResponseMessages); + + // ...and on the client (source) too: upstream TestStatsGrpcStream asserts the + // in-flight message counters on BOTH admin ports, so a client-side periodic-reporting + // regression must also fail here. The client sidecar carries the same grpc_stats + + // short-interval istio.stats, so its timer publishes the same in-flight deltas. + auto mid_req_client = waitForMsgCount(test_server_->statStore(), "istio_request_messages_total", + kFirstRequestMessages); + ASSERT_NE(mid_req_client, nullptr); + EXPECT_GE(mid_req_client->value(), kFirstRequestMessages); + auto mid_resp_client = waitForMsgCount(test_server_->statStore(), "istio_response_messages_total", + kFirstResponseMessages); + ASSERT_NE(mid_resp_client, nullptr); + EXPECT_GE(mid_resp_client->value(), kFirstResponseMessages); + + // Close the stream: one final response message + trailers, end the request. + Buffer::OwnedImpl last_response = grpcFrame("response-final"); + upstream_request_->encodeData(last_response, false); + upstream_request_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); + Buffer::OwnedImpl empty; + codec_client_->sendData(*request_encoder_, empty, /*end_stream=*/true); + ASSERT_TRUE(response->waitForEndStream()); + codec_client_->close(); + + // Final totals after close, on both sidecars. + auto final_req = serverCounter("istio_request_messages_total"); + ASSERT_NE(final_req, nullptr); + EXPECT_EQ(kFirstRequestMessages, final_req->value()); + auto final_resp = serverCounter("istio_response_messages_total"); + ASSERT_NE(final_resp, nullptr); + EXPECT_EQ(kFirstResponseMessages + 1, final_resp->value()); + + auto final_req_client = clientCounter("istio_request_messages_total"); + ASSERT_NE(final_req_client, nullptr); + EXPECT_EQ(kFirstRequestMessages, final_req_client->value()); + auto final_resp_client = clientCounter("istio_response_messages_total"); + ASSERT_NE(final_resp_client, nullptr); + EXPECT_EQ(kFirstResponseMessages + 1, final_resp_client->value()); +} + +// --- ECDS (migrates TestStatsECDS) --- +// +// The SERVER sidecar's istio.stats filter is delivered via Extension Config +// Discovery (a file path_config_source) rather than inline typed_config, and the +// resulting istio_requests_total must still carry reporter=destination with the +// source identity from the real client-sidecar handshake. + +// Related upstream e2e: stats_plugin/TestStatsECDS (warming path). +// Warming ECDS: the listener warms on the discovered config; after config_reload +// fires we drive traffic and assert the server metric. +TEST_P(IstioTwoProxyIntegrationTest, EcdsWarming) { + TestEnvironment::writeStringToFileForTest("istio_stats_ecds.yaml", R"EOF( +resources: +- "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: envoy.filters.http.istio_stats + typed_config: + "@type": type.googleapis.com/stats.PluginConfig +)EOF", + /*fully_qualified_path=*/false); + server_stats_filter_override_ = TestEnvironment::substitute(R"EOF( +name: envoy.filters.http.istio_stats +config_discovery: + type_urls: + - type.googleapis.com/stats.PluginConfig + config_source: + path_config_source: + path: "{{ test_tmpdir }}/istio_stats_ecds.yaml" +)EOF"); + initializeTwoProxies(); + server_sidecar_->waitForCounter( + "extension_config_discovery.http_filter.envoy.filters.http.istio_stats.config_reload", + testing::Ge(1)); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server, "destination_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsECDS (default_config_without_warming case). +// default_config + apply_default_config_without_warming: the filter is active +// immediately with no discovery round-trip. +TEST_P(IstioTwoProxyIntegrationTest, EcdsDefaultConfigWithoutWarming) { + TestEnvironment::writeStringToFileForTest("istio_stats_ecds_empty.yaml", "resources: []", + /*fully_qualified_path=*/false); + server_stats_filter_override_ = TestEnvironment::substitute(R"EOF( +name: envoy.filters.http.istio_stats +config_discovery: + type_urls: + - type.googleapis.com/stats.PluginConfig + apply_default_config_without_warming: true + default_config: + "@type": type.googleapis.com/stats.PluginConfig + config_source: + path_config_source: + path: "{{ test_tmpdir }}/istio_stats_ecds_empty.yaml" +)EOF"); + initializeTwoProxies(); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ(1, server->value()); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server, "destination_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsECDS. +// TestStatsECDS, both sidecars: istio.stats is delivered via ECDS (config_discovery) on +// BOTH the client (outbound) and server (inbound), and istio_requests_total must carry the +// right reporter/identities on each. EcdsWarming only covered the server side; this closes +// the client-side ECDS path. Note: delivery is via a file path_config_source rather than +// gRPC/ADS, so the ECDS config-application and reload logic is exercised on both sides but +// not the xDS transport itself. +TEST_P(IstioTwoProxyIntegrationTest, EcdsBothSidecars) { + TestEnvironment::writeStringToFileForTest("istio_stats_ecds_both.yaml", R"EOF( +resources: +- "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: envoy.filters.http.istio_stats + typed_config: + "@type": type.googleapis.com/stats.PluginConfig +)EOF", + /*fully_qualified_path=*/false); + const std::string ecds_override = TestEnvironment::substitute(R"EOF( +name: envoy.filters.http.istio_stats +config_discovery: + type_urls: + - type.googleapis.com/stats.PluginConfig + config_source: + path_config_source: + path: "{{ test_tmpdir }}/istio_stats_ecds_both.yaml" +)EOF"); + client_stats_filter_override_ = ecds_override; + server_stats_filter_override_ = ecds_override; + initializeTwoProxies(); + + const std::string reload_counter = + "extension_config_discovery.http_filter.envoy.filters.http.istio_stats.config_reload"; + test_server_->waitForCounter(reload_counter, testing::Ge(1)); + server_sidecar_->waitForCounter(reload_counter, testing::Ge(1)); + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + + // Client (outbound) filter delivered via ECDS records the source-reporter stat. + auto client = clientCounter("istio_requests_total"); + ASSERT_NE(client, nullptr); + EXPECT_EQ("source", tagValue(*client, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*client, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*client, "destination_workload").value_or("")); + + // Server (inbound) filter delivered via ECDS records the destination-reporter stat. + auto server = serverCounter("istio_requests_total"); + ASSERT_NE(server, nullptr); + EXPECT_EQ("destination", tagValue(*server, "reporter").value_or("")); + EXPECT_EQ("client-v1", tagValue(*server, "source_workload").value_or("")); + EXPECT_EQ("server-v1", tagValue(*server, "destination_workload").value_or("")); +} + +// Related upstream e2e: stats_plugin/TestStatsParallel. +// TestStatsParallel: stats-config pushes *concurrent* with in-flight traffic, on BOTH +// sidecars (upstream pushes new listener versions to both the client and the server +// nodes during the run). Each sidecar's istio.stats is delivered via ECDS (a file +// path_config_source, which reloads when a new file is moved onto the watched path). +// Two background threads each rename a stream of pre-written resource files -- alternating +// between config v1 (standard metrics) and config v2 (adds a custom `my_count` definition) +// -- onto the client's and the server's watched paths respectively, while the main thread +// drives a steady stream of requests through both sidecars. This reproduces the +// istio/proxy Fork-based race (config reload on the main thread vs. request handling on a +// worker thread) on each side rather than a single serial swap; it must not crash and both +// configs must take effect on both sidecars. +TEST_P(IstioTwoProxyIntegrationTest, StatsConfigPushUnderTraffic) { + const std::string v0 = R"EOF( +resources: +- "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: envoy.filters.http.istio_stats + typed_config: + "@type": type.googleapis.com/stats.PluginConfig +)EOF"; + const std::string client_watched = TestEnvironment::writeStringToFileForTest( + "istio_stats_parallel_client.yaml", v0, /*fully_qualified_path=*/false); + const std::string server_watched = TestEnvironment::writeStringToFileForTest( + "istio_stats_parallel_server.yaml", v0, /*fully_qualified_path=*/false); + client_stats_filter_override_ = TestEnvironment::substitute(R"EOF( +name: envoy.filters.http.istio_stats +config_discovery: + type_urls: + - type.googleapis.com/stats.PluginConfig + config_source: + path_config_source: + path: "{{ test_tmpdir }}/istio_stats_parallel_client.yaml" +)EOF"); + server_stats_filter_override_ = TestEnvironment::substitute(R"EOF( +name: envoy.filters.http.istio_stats +config_discovery: + type_urls: + - type.googleapis.com/stats.PluginConfig + config_source: + path_config_source: + path: "{{ test_tmpdir }}/istio_stats_parallel_server.yaml" +)EOF"); + initializeTwoProxies(); + + const std::string reload_counter = + "extension_config_discovery.http_filter.envoy.filters.http.istio_stats.config_reload"; + test_server_->waitForCounter(reload_counter, testing::Ge(1)); + server_sidecar_->waitForCounter(reload_counter, testing::Ge(1)); + + // The two config variants. v2 adds a custom `my_count` definition. + const std::string& v1 = v0; + const std::string v2 = R"EOF( +resources: +- "@type": type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig + name: envoy.filters.http.istio_stats + typed_config: + "@type": type.googleapis.com/stats.PluginConfig + definitions: + - name: my_count + type: COUNTER + value: "1" +)EOF"; + + // Pre-write the swap source files on the main thread (TestEnvironment file helpers + // are not thread-safe); the background threads only perform std::rename (an atomic + // libc call) onto the watched paths. Distinct source files per side so the two + // renamers never contend. + constexpr int kSwaps = 40; + std::vector client_swaps; + std::vector server_swaps; + client_swaps.reserve(kSwaps); + server_swaps.reserve(kSwaps); + for (int i = 0; i < kSwaps; ++i) { + client_swaps.push_back(TestEnvironment::writeStringToFileForTest( + absl::StrCat("istio_stats_parallel_client_src_", i, ".yaml"), (i % 2 == 0) ? v1 : v2, + /*fully_qualified_path=*/false)); + server_swaps.push_back(TestEnvironment::writeStringToFileForTest( + absl::StrCat("istio_stats_parallel_server_src_", i, ".yaml"), (i % 2 == 0) ? v1 : v2, + /*fully_qualified_path=*/false)); + } + + std::atomic rename_failed{false}; + const auto rename_loop = [&rename_failed](const std::vector& srcs, + const std::string& dst) { + for (const auto& f : srcs) { + if (std::rename(f.c_str(), dst.c_str()) != 0) { + rename_failed.store(true); + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(3)); // NO_CHECK_FORMAT(real_time) + } + }; + std::thread client_pusher([&]() { rename_loop(client_swaps, client_watched); }); + std::thread server_pusher([&]() { rename_loop(server_swaps, server_watched); }); + + // Drive a steady stream of requests while the config churns underneath both sidecars. + // Requests must keep succeeding across live ECDS reloads (runRequest asserts each + // completes). + constexpr int kRequests = 40; + for (int i = 0; i < kRequests; ++i) { + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + } + client_pusher.join(); + server_pusher.join(); + ASSERT_FALSE(rename_failed.load()); + + // Settle both sides on a known-final config (v2) and confirm both the standard and the + // custom metric are emitted on each sidecar after the storm -- proving config pushes + // under traffic neither crashed nor wedged either filter. Poll because the file watcher + // reload is async. + const std::string client_final = TestEnvironment::writeStringToFileForTest( + "istio_stats_parallel_client_final.yaml", v2, /*fully_qualified_path=*/false); + const std::string server_final = TestEnvironment::writeStringToFileForTest( + "istio_stats_parallel_server_final.yaml", v2, /*fully_qualified_path=*/false); + ASSERT_EQ(0, std::rename(client_final.c_str(), client_watched.c_str())); + ASSERT_EQ(0, std::rename(server_final.c_str(), server_watched.c_str())); + + Stats::CounterSharedPtr client_my_count; + Stats::CounterSharedPtr server_my_count; + for (int i = 0; i < 50 && (client_my_count == nullptr || server_my_count == nullptr); ++i) { + runRequest("GET", "/", "server-svc.server-ns.svc.cluster.local"); + client_my_count = clientCounter("istio_my_count"); + server_my_count = serverCounter("istio_my_count"); + } + ASSERT_NE(client_my_count, nullptr); + EXPECT_GE(client_my_count->value(), 1); + ASSERT_NE(server_my_count, nullptr); + EXPECT_GE(server_my_count->value(), 1); + + auto client_requests = clientCounter("istio_requests_total"); + ASSERT_NE(client_requests, nullptr); + EXPECT_GE(client_requests->value(), 1); + auto server_requests = serverCounter("istio_requests_total"); + ASSERT_NE(server_requests, nullptr); + EXPECT_GE(server_requests->value(), 1); +} + +} // namespace +} // namespace Envoy diff --git a/contrib/istio/filters/http/peer_metadata/source/peer_metadata.cc b/contrib/istio/filters/http/peer_metadata/source/peer_metadata.cc index 76950ef436e8b..d469d9e640955 100644 --- a/contrib/istio/filters/http/peer_metadata/source/peer_metadata.cc +++ b/contrib/istio/filters/http/peer_metadata/source/peer_metadata.cc @@ -1,5 +1,7 @@ #include "contrib/istio/filters/http/peer_metadata/source/peer_metadata.h" +#include + #include "envoy/registry/registry.h" #include "envoy/server/factory_context.h" @@ -20,10 +22,10 @@ class XDSMethod : public DiscoveryMethod { public: XDSMethod(bool downstream, Server::Configuration::ServerFactoryContext& factory_context) : downstream_(downstream), - metadata_provider_(Extensions::Common::WorkloadDiscovery::getProvider(factory_context)), + metadata_provider_(Extensions::Common::WorkloadDiscovery::GetProvider(factory_context)), local_info_(factory_context.localInfo()) {} - absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, - Context&) const override; + std::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; private: const bool downstream_; @@ -31,8 +33,8 @@ class XDSMethod : public DiscoveryMethod { const LocalInfo::LocalInfo& local_info_; }; -absl::optional XDSMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, - Http::HeaderMap& headers, Context&) const { +std::optional XDSMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, + Http::HeaderMap& headers, Context&) const { if (!metadata_provider_) { return {}; } @@ -98,7 +100,7 @@ absl::optional XDSMethod::derivePeerInfo(const StreamInfo::StreamInfo& return {}; } ENVOY_LOG_MISC(debug, "Peer address: {}", peer_address->asString()); - return metadata_provider_->getMetadata(peer_address); + return metadata_provider_->GetMetadata(peer_address); } MXMethod::MXMethod(bool downstream, const absl::flat_hash_set additional_labels, @@ -108,8 +110,8 @@ MXMethod::MXMethod(bool downstream, const absl::flat_hash_set addit tls_.set([](Event::Dispatcher&) { return std::make_shared(); }); } -absl::optional MXMethod::derivePeerInfo(const StreamInfo::StreamInfo&, - Http::HeaderMap& headers, Context& ctx) const { +std::optional MXMethod::derivePeerInfo(const StreamInfo::StreamInfo&, + Http::HeaderMap& headers, Context& ctx) const { const auto peer_id_header = headers.get(Headers::get().ExchangeMetadataHeaderId); if (downstream_) { ctx.request_peer_id_received_ = !peer_id_header.empty(); @@ -133,7 +135,7 @@ void MXMethod::remove(Http::HeaderMap& headers) const { headers.remove(Headers::get().ExchangeMetadataHeader); } -absl::optional MXMethod::lookup(absl::string_view id, absl::string_view value) const { +std::optional MXMethod::lookup(absl::string_view id, absl::string_view value) const { // This code is copied from: // https://github.com/istio/proxy/blob/release-1.18/extensions/metadata_exchange/plugin.cc#L116 auto& cache = tls_->cache_; @@ -144,7 +146,7 @@ absl::optional MXMethod::lookup(absl::string_view id, absl::string_vie } } const auto bytes = Base64::decodeWithoutPadding(value); - Protobuf::Struct metadata; + Envoy::Protobuf::Struct metadata; if (!metadata.ParseFromString(bytes)) { return {}; } @@ -159,38 +161,19 @@ absl::optional MXMethod::lookup(absl::string_view id, absl::string_vie return *out; } -MXPropagationMethod::MXPropagationMethod( - bool downstream, Server::Configuration::ServerFactoryContext& factory_context, - const absl::flat_hash_set& additional_labels, - const io::istio::http::peer_metadata::Config_IstioHeaders& istio_headers) - : downstream_(downstream), id_(factory_context.localInfo().node().id()), - value_(computeValue(additional_labels, factory_context)), - skip_external_clusters_(istio_headers.skip_external_clusters()) {} - -std::string MXPropagationMethod::computeValue( - const absl::flat_hash_set& additional_labels, - Server::Configuration::ServerFactoryContext& factory_context) const { - const auto obj = Istio::Common::convertStructToWorkloadMetadata( - factory_context.localInfo().node().metadata(), additional_labels, - factory_context.localInfo().node().locality()); - const Protobuf::Struct metadata = Istio::Common::convertWorkloadMetadataToStruct(*obj); - const std::string metadata_bytes = Istio::Common::serializeToStringDeterministic(metadata); - return Base64::encode(metadata_bytes.data(), metadata_bytes.size()); -} - class UpstreamFilterStateMethod : public DiscoveryMethod { public: UpstreamFilterStateMethod( const io::istio::http::peer_metadata::Config_UpstreamFilterState& config) : peer_metadata_key_(config.peer_metadata_key()) {} - absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, - Context&) const override; + std::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; private: std::string peer_metadata_key_; }; -absl::optional +std::optional UpstreamFilterStateMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, Http::HeaderMap&, Context&) const { const auto upstream = info.upstreamInfo(); @@ -210,12 +193,12 @@ UpstreamFilterStateMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, Ht return {}; } - Protobuf::Struct obj; + Envoy::Protobuf::Struct obj; if (!obj.ParseFromString(absl::string_view(cel_state->value()))) { return {}; } - std::unique_ptr peer_info = Istio::Common::convertStructToWorkloadMetadata(obj); + std::unique_ptr peer_info = ::Istio::Common::convertStructToWorkloadMetadata(obj); if (!peer_info) { return {}; } @@ -223,6 +206,38 @@ UpstreamFilterStateMethod::derivePeerInfo(const StreamInfo::StreamInfo& info, Ht return *peer_info; } +MXPropagationMethod::MXPropagationMethod( + bool downstream, Server::Configuration::ServerFactoryContext& factory_context, + const absl::flat_hash_set& additional_labels, + const io::istio::http::peer_metadata::Config_IstioHeaders& istio_headers) + : downstream_(downstream), id_(factory_context.localInfo().node().id()), + value_(computeValue(additional_labels, factory_context)), + skip_external_clusters_(istio_headers.skip_external_clusters()) {} + +std::string MXPropagationMethod::computeValue( + const absl::flat_hash_set& additional_labels, + Server::Configuration::ServerFactoryContext& factory_context) const { + const auto obj = Istio::Common::convertStructToWorkloadMetadata( + factory_context.localInfo().node().metadata(), additional_labels, + factory_context.localInfo().node().locality()); + const Envoy::Protobuf::Struct metadata = Istio::Common::convertWorkloadMetadataToStruct(*obj); + const std::string metadata_bytes = Istio::Common::serializeToStringDeterministic(metadata); + return Base64::encode(metadata_bytes.data(), metadata_bytes.size()); +} + +void MXPropagationMethod::inject(const StreamInfo::StreamInfo& info, Http::HeaderMap& headers, + Context& ctx) const { + if (skipMXHeaders(skip_external_clusters_, info)) { + return; + } + if (!downstream_ || ctx.request_peer_id_received_) { + headers.setReference(Headers::get().ExchangeMetadataHeaderId, id_); + } + if (!downstream_ || ctx.request_peer_received_) { + headers.setReference(Headers::get().ExchangeMetadataHeader, value_); + } +} + BaggagePropagationMethod::BaggagePropagationMethod( Server::Configuration::ServerFactoryContext& factory_context, const io::istio::http::peer_metadata::Config_Baggage&) @@ -241,11 +256,11 @@ void BaggagePropagationMethod::inject(const StreamInfo::StreamInfo&, Http::Heade headers.setReference(Headers::get().Baggage, value_); } -BaggageDiscoveryMethod::BaggageDiscoveryMethod() {} +BaggageDiscoveryMethod::BaggageDiscoveryMethod() = default; -absl::optional BaggageDiscoveryMethod::derivePeerInfo(const StreamInfo::StreamInfo&, - Http::HeaderMap& headers, - Context&) const { +std::optional BaggageDiscoveryMethod::derivePeerInfo(const StreamInfo::StreamInfo&, + Http::HeaderMap& headers, + Context&) const { const auto baggage_header = headers.get(Headers::get().Baggage); if (baggage_header.empty()) { return {}; @@ -258,19 +273,6 @@ absl::optional BaggageDiscoveryMethod::derivePeerInfo(const StreamInfo return {}; } -void MXPropagationMethod::inject(const StreamInfo::StreamInfo& info, Http::HeaderMap& headers, - Context& ctx) const { - if (skipMXHeaders(skip_external_clusters_, info)) { - return; - } - if (!downstream_ || ctx.request_peer_id_received_) { - headers.setReference(Headers::get().ExchangeMetadataHeaderId, id_); - } - if (!downstream_ || ctx.request_peer_received_) { - headers.setReference(Headers::get().ExchangeMetadataHeader, value_); - } -} - FilterConfig::FilterConfig(const io::istio::http::peer_metadata::Config& config, Server::Configuration::FactoryContext& factory_context) : shared_with_upstream_(config.shared_with_upstream()), @@ -415,17 +417,17 @@ void FilterConfig::setFilterState(StreamInfo::StreamInfo& info, bool downstream, auto pb = value.serializeAsProto(); auto cel_state = std::make_unique(FilterConfig::peerInfoPrototype()); cel_state->setValue(absl::string_view(pb->SerializeAsString())); - info.filterState()->setData( - key, std::move(cel_state), StreamInfo::FilterState::StateType::Mutable, - StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); + info.filterState()->setData(key, std::move(cel_state), + StreamInfo::FilterState::LifeSpan::FilterChain, + sharedWithUpstream()); // Also store WorkloadMetadataObject under a separate key for FIELD accessor support. // WorkloadMetadataObject implements hasFieldSupport() + getField() for // formatters using %FILTER_STATE(downstream_peer_obj:FIELD:fieldname)% syntax. auto workload_metadata = std::make_unique(value); - info.filterState()->setData( - obj_key, std::move(workload_metadata), StreamInfo::FilterState::StateType::Mutable, - StreamInfo::FilterState::LifeSpan::FilterChain, sharedWithUpstream()); + info.filterState()->setData(obj_key, std::move(workload_metadata), + StreamInfo::FilterState::LifeSpan::FilterChain, + sharedWithUpstream()); } else { ENVOY_LOG(debug, "Duplicate peer metadata, skipping"); } diff --git a/contrib/istio/filters/http/peer_metadata/source/peer_metadata.h b/contrib/istio/filters/http/peer_metadata/source/peer_metadata.h index e8182892e2567..267ba7edcf8f1 100644 --- a/contrib/istio/filters/http/peer_metadata/source/peer_metadata.h +++ b/contrib/istio/filters/http/peer_metadata/source/peer_metadata.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "source/common/singleton/const_singleton.h" #include "source/extensions/filters/common/expr/cel_state.h" #include "source/extensions/filters/http/common/factory_base.h" @@ -38,8 +40,8 @@ struct Context { class DiscoveryMethod { public: virtual ~DiscoveryMethod() = default; - virtual absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, - Context&) const PURE; + virtual std::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const PURE; virtual void remove(Http::HeaderMap&) const {} }; @@ -49,12 +51,12 @@ class MXMethod : public DiscoveryMethod { public: MXMethod(bool downstream, const absl::flat_hash_set additional_labels, Server::Configuration::ServerFactoryContext& factory_context); - absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, - Context&) const override; + std::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; void remove(Http::HeaderMap&) const override; private: - absl::optional lookup(absl::string_view id, absl::string_view value) const; + std::optional lookup(absl::string_view id, absl::string_view value) const; const bool downstream_; struct MXCache : public ThreadLocal::ThreadLocalObject { absl::flat_hash_map cache_; @@ -104,8 +106,8 @@ class BaggagePropagationMethod : public PropagationMethod { class BaggageDiscoveryMethod : public DiscoveryMethod, public Logger::Loggable { public: BaggageDiscoveryMethod(); - absl::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, - Context&) const override; + std::optional derivePeerInfo(const StreamInfo::StreamInfo&, Http::HeaderMap&, + Context&) const override; }; class FilterConfig : public Logger::Loggable { diff --git a/contrib/istio/filters/http/peer_metadata/test/BUILD b/contrib/istio/filters/http/peer_metadata/test/BUILD index 4500d57b2ed03..6e654b73f71ab 100644 --- a/contrib/istio/filters/http/peer_metadata/test/BUILD +++ b/contrib/istio/filters/http/peer_metadata/test/BUILD @@ -30,3 +30,18 @@ envoy_cc_test( "//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "peer_metadata_integration_test", + srcs = ["peer_metadata_integration_test.cc"], + deps = [ + "//contrib/istio/filters/common/source:metadata_object_lib", + "//contrib/istio/filters/http/peer_metadata/source:config", + "//contrib/istio/filters/network/metadata_exchange/source:config", + "//source/common/common:base64_lib", + "//source/extensions/filters/http/router:config", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + ], +) diff --git a/contrib/istio/filters/http/peer_metadata/test/filter_test.cc b/contrib/istio/filters/http/peer_metadata/test/filter_test.cc index 331bc1a3933c9..a98c46f57e57f 100644 --- a/contrib/istio/filters/http/peer_metadata/test/filter_test.cc +++ b/contrib/istio/filters/http/peer_metadata/test/filter_test.cc @@ -1,3 +1,5 @@ +#include + #include "source/common/network/address_impl.h" #include "test/common/stream_info/test_util.h" @@ -9,7 +11,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using Envoy::Istio::Common::WorkloadMetadataObject; +using Istio::Common::WorkloadMetadataObject; using testing::HasSubstr; using testing::Invoke; using testing::Return; @@ -33,7 +35,7 @@ class MockWorkloadMetadataProvider public Singleton::Instance { public: MockWorkloadMetadataProvider() = default; - MOCK_METHOD(absl::optional, getMetadata, + MOCK_METHOD(std::optional, GetMetadata, (const Network::Address::InstanceConstSharedPtr& address)); }; @@ -83,7 +85,7 @@ class PeerMetadataTest : public testing::Test { void checkShared(bool expected) { EXPECT_EQ(expected, - stream_info_.filterState()->objectsSharedWithUpstreamConnection()->size() > 0); + !stream_info_.filterState()->objectsSharedWithUpstreamConnection()->empty()); } NiceMock context_; NiceMock singleton_manager_; @@ -105,7 +107,7 @@ TEST_F(PeerMetadataTest, None) { } TEST_F(PeerMetadataTest, DownstreamXDSNone) { - EXPECT_CALL(*metadata_provider_, getMetadata(_)).WillRepeatedly(Return(std::nullopt)); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)).WillRepeatedly(Return(std::nullopt)); initialize(R"EOF( downstream_discovery: - workload_discovery: {} @@ -118,10 +120,11 @@ TEST_F(PeerMetadataTest, DownstreamXDSNone) { TEST_F(PeerMetadataTest, DownstreamXDS) { const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "127.0.0.1")) { return {pod}; } @@ -140,10 +143,11 @@ TEST_F(PeerMetadataTest, DownstreamXDS) { TEST_F(PeerMetadataTest, UpstreamXDS) { const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "foo", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "10.0.0.1")) { return {pod}; } @@ -176,10 +180,11 @@ TEST_F(PeerMetadataTest, UpstreamXDSInternal) { *host_metadata); const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "foo", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "127.0.0.100")) { return {pod}; } @@ -231,7 +236,7 @@ constexpr absl::string_view SampleIstioHeader = TEST_F(PeerMetadataTest, DownstreamFallbackFirst) { request_headers_.setReference(Headers::get().ExchangeMetadataHeaderId, "test-pod"); request_headers_.setReference(Headers::get().ExchangeMetadataHeader, SampleIstioHeader); - EXPECT_CALL(*metadata_provider_, getMetadata(_)).Times(0); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)).Times(0); initialize(R"EOF( downstream_discovery: - istio_headers: {} @@ -245,10 +250,11 @@ TEST_F(PeerMetadataTest, DownstreamFallbackFirst) { TEST_F(PeerMetadataTest, DownstreamFallbackSecond) { const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "default", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "127.0.0.1")) { // remote address return {pod}; } @@ -312,7 +318,7 @@ TEST_F(PeerMetadataTest, UpstreamMX) { } TEST_F(PeerMetadataTest, UpstreamFallbackFirst) { - EXPECT_CALL(*metadata_provider_, getMetadata(_)).Times(0); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)).Times(0); response_headers_.setReference(Headers::get().ExchangeMetadataHeaderId, "test-pod"); response_headers_.setReference(Headers::get().ExchangeMetadataHeader, SampleIstioHeader); initialize(R"EOF( @@ -328,10 +334,11 @@ TEST_F(PeerMetadataTest, UpstreamFallbackFirst) { TEST_F(PeerMetadataTest, UpstreamFallbackSecond) { const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "foo", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "10.0.0.1")) { // upstream host address return {pod}; } @@ -350,10 +357,11 @@ TEST_F(PeerMetadataTest, UpstreamFallbackSecond) { TEST_F(PeerMetadataTest, UpstreamFallbackFirstXDS) { const WorkloadMetadataObject pod("pod-foo-1234", "my-cluster", "foo", "foo", "foo-service", - "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, ""); - EXPECT_CALL(*metadata_provider_, getMetadata(_)) + "v1alpha3", "", "", Istio::Common::WorkloadType::Pod, "", "", + ""); + EXPECT_CALL(*metadata_provider_, GetMetadata(_)) .WillRepeatedly(Invoke([&](const Network::Address::InstanceConstSharedPtr& address) - -> absl::optional { + -> std::optional { if (absl::StartsWith(address->asStringView(), "10.0.0.1")) { // upstream host address return {pod}; } diff --git a/contrib/istio/filters/http/peer_metadata/test/peer_metadata_integration_test.cc b/contrib/istio/filters/http/peer_metadata/test/peer_metadata_integration_test.cc new file mode 100644 index 0000000000000..b7b363040e51a --- /dev/null +++ b/contrib/istio/filters/http/peer_metadata/test/peer_metadata_integration_test.cc @@ -0,0 +1,185 @@ +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" + +#include "source/common/common/base64.h" + +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "absl/strings/str_cat.h" +#include "contrib/istio/filters/common/source/metadata_object.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +using Istio::Common::WorkloadMetadataObject; +using Istio::Common::WorkloadType; + +// Integration coverage for the HTTP peer_metadata filter, migrating the istio/proxy +// `test/envoye2e/http_metadata_exchange` suite (mx_native_inbound configures +// io.istio.http.peer_metadata.Config). A server-side (inbound) Envoy runs the +// filter with istio_headers downstream discovery + propagation; the test harness +// plays the client by sending crafted x-envoy-peer-metadata[-id] request headers +// and asserts the conditional response headers. +class PeerMetadataHttpIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + PeerMetadataHttpIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + + void initializeFilter(const std::string& additional_labels = "", bool with_network_mx = false) { + // Use the bootstrap node verbatim (otherwise the test framework overrides + // node.id with its default "node_name"). + use_bootstrap_node_metadata_ = true; + if (with_network_mx) { + // TestNativeHTTPExchange wires a TCP metadata_exchange network filter ahead of + // the HCM to prove "TCP MX must not break HTTP MX when there is no TCP prefix or + // TCP MX ALPN". Over this plaintext HTTP/1 listener the ALPN protocol is never + // "istio2", so the network MX finds no match, marks no-peer, and passes the bytes + // through to the HCM unharmed -- the HTTP MX handshake must still succeed. + config_helper_.addNetworkFilter(R"EOF( +name: envoy.filters.network.metadata_exchange +typed_config: + "@type": type.googleapis.com/envoy.tcp.metadataexchange.config.MetadataExchange + protocol: istio2 +)EOF"); + } + // Node identity: the filter propagates node().id() as x-envoy-peer-metadata-id + // and the node metadata as the x-envoy-peer-metadata blob. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto& node = *bootstrap.mutable_node(); + node.set_id("server"); + node.set_cluster("server-cluster"); + auto& fields = *node.mutable_metadata()->mutable_fields(); + fields["WORKLOAD_NAME"].set_string_value("server-v1"); + fields["NAMESPACE"].set_string_value("server-ns"); + fields["CLUSTER_ID"].set_string_value("server-cluster"); + }); + config_helper_.prependFilter(absl::StrCat(R"EOF( +name: envoy.filters.http.peer_metadata +typed_config: + "@type": type.googleapis.com/io.istio.http.peer_metadata.Config + downstream_discovery: + - istio_headers: {} + downstream_propagation: + - istio_headers: {} +)EOF", + additional_labels)); + initialize(); + } + + // Encodes a workload as the x-envoy-peer-metadata wire value (base64 of a + // deterministically serialized Struct). + static std::string peerMetadata() { + const WorkloadMetadataObject client("client-pod", "client-cluster", "client-ns", "client-v1", + "client-svc", "v1", "client-app", "v1", WorkloadType::Pod, + "spiffe://client", "", ""); + const Protobuf::Struct metadata = Istio::Common::convertWorkloadMetadataToStruct(client); + const std::string bytes = Istio::Common::serializeToStringDeterministic(metadata); + return Base64::encode(bytes.data(), bytes.size()); + } + + // Drives one request with the given extra request headers, returns the response. + IntegrationStreamDecoderPtr + exchange(const std::vector>& extra_request_headers) { + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + for (const auto& h : extra_request_headers) { + request.addCopy(h.first, h.second); + } + auto response = codec_client_->makeHeaderOnlyRequest(request); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + EXPECT_TRUE(response->waitForEndStream()); + codec_client_->close(); + return response; + } + + static bool hasHeader(IntegrationStreamDecoder& response, const std::string& name) { + return !response.headers().get(Http::LowerCaseString(name)).empty(); + } + static std::string headerValue(IntegrationStreamDecoder& response, const std::string& name) { + auto h = response.headers().get(Http::LowerCaseString(name)); + return h.empty() ? "" : std::string(h[0]->value().getStringView()); + } + + // Drives 1000 exchanges with unique peer IDs (matching upstream's N=1000, "high + // enough to exercise cache eviction" -- above the MX peer cache size), asserting + // a correct exchange each time and no Envoy bug-failures. `server.envoy_bug_failures` + // is lazily created, so absent == zero bugs == pass; if present it must be 0. + void runExchangeLoopAndCheckNoBugFailures() { + constexpr int kRequests = 1000; + for (int i = 0; i < kRequests; ++i) { + auto response = exchange({{"x-envoy-peer-metadata-id", absl::StrCat("client", i)}, + {"x-envoy-peer-metadata", peerMetadata()}}); + EXPECT_EQ("server", headerValue(*response, "x-envoy-peer-metadata-id")); + EXPECT_TRUE(hasHeader(*response, "x-envoy-peer-metadata")); + } + auto bug_failures = test_server_->counter("server.envoy_bug_failures"); + if (bug_failures != nullptr) { + EXPECT_EQ(0, bug_failures->value()); + } + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, PeerMetadataHttpIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Related upstream e2e: http_metadata_exchange/TestHTTPExchange. +// TestHTTPExchange: the server emits its peer metadata on the response only in +// proportion to what the client sent (the conditional-injection contract). +TEST_P(PeerMetadataHttpIntegrationTest, ConditionalResponseEmission) { + initializeFilter(); + + // 1. No client headers -> server emits neither response header. + { + auto response = exchange({}); + EXPECT_FALSE(hasHeader(*response, "x-envoy-peer-metadata-id")); + EXPECT_FALSE(hasHeader(*response, "x-envoy-peer-metadata")); + } + // 2. Only the ID -> server emits its ID but withholds the metadata blob. + { + auto response = exchange({{"x-envoy-peer-metadata-id", "client"}}); + EXPECT_EQ("server", headerValue(*response, "x-envoy-peer-metadata-id")); + EXPECT_FALSE(hasHeader(*response, "x-envoy-peer-metadata")); + } + // 3. Full exchange -> server emits both its ID and metadata blob. + { + auto response = exchange( + {{"x-envoy-peer-metadata-id", "client"}, {"x-envoy-peer-metadata", peerMetadata()}}); + EXPECT_EQ("server", headerValue(*response, "x-envoy-peer-metadata-id")); + EXPECT_TRUE(hasHeader(*response, "x-envoy-peer-metadata")); + } +} + +// Related upstream e2e: http_metadata_exchange/TestNativeHTTPExchange. +// TestNativeHTTPExchange: a TCP metadata_exchange network filter sits ahead of the +// HCM (it must not break HTTP MX when no TCP MX ALPN is negotiated), and many requests +// with unique peer IDs exercise the MX cache (eviction); the exchange stays correct +// and no Envoy bug-failures occur. +TEST_P(PeerMetadataHttpIntegrationTest, CacheEvictionNoBugFailures) { + initializeFilter(/*additional_labels=*/"", /*with_network_mx=*/true); + runExchangeLoopAndCheckNoBugFailures(); +} + +// Related upstream e2e: http_metadata_exchange/TestHTTPExchangeAdditionalLabels. +// TestHTTPExchangeAdditionalLabels: structurally identical to TestNativeHTTPExchange +// (the TCP metadata_exchange network filter sits ahead of the HCM, exactly as upstream +// sets ServerNetworkFilters=server_mx_network_filter for this case) but with the +// additional_labels filter variant -- same eviction load + bug-failure guard, exercised +// under the labels config with network MX present. This guards the regression where +// network MX interferes specifically with *labeled* HTTP MX. +TEST_P(PeerMetadataHttpIntegrationTest, AdditionalLabels) { + initializeFilter(R"EOF( additional_labels: + - role +)EOF", + /*with_network_mx=*/true); + runExchangeLoopAndCheckNoBugFailures(); +} + +} // namespace +} // namespace Envoy diff --git a/contrib/istio/filters/network/metadata_exchange/source/config.cc b/contrib/istio/filters/network/metadata_exchange/source/config.cc index 887470b980c09..cd052815b1159 100644 --- a/contrib/istio/filters/network/metadata_exchange/source/config.cc +++ b/contrib/istio/filters/network/metadata_exchange/source/config.cc @@ -6,8 +6,7 @@ #include "contrib/istio/filters/network/metadata_exchange/source/metadata_exchange.h" namespace Envoy { -namespace Extensions { -namespace NetworkFilters { +namespace Tcp { namespace MetadataExchange { namespace { @@ -88,6 +87,5 @@ static Registry::RegisterFactoryadditional_labels_, local_info_.node().locality()); + *(*data.mutable_fields())[ExchangeMetadataHeader].mutable_struct_value() = Istio::Common::convertWorkloadMetadataToStruct(*obj); std::string metadata_id = getMetadataId(); @@ -277,13 +277,11 @@ void MetadataExchangeFilter::updatePeer(const Istio::Common::WorkloadMetadataObj auto& filter_state = *read_callbacks_->connection().streamInfo().filterState(); filter_state.setData(filter_state_key, std::move(peer_info), - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Connection); // Also store WorkloadMetadataObject for FIELD accessor and peerInfoRead() detection auto workload_metadata = std::make_unique(value); filter_state.setData(obj_key, std::move(workload_metadata), - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Connection); } @@ -326,7 +324,7 @@ void MetadataExchangeFilter::setMetadataNotFoundFilterState() { auto downstream_peer_address = [&]() -> Network::Address::InstanceConstSharedPtr { if (upstream_peer) { // Query upstream peer data and save it in metadata for stats - const auto metadata_object = config_->metadata_provider_->getMetadata(upstream_peer); + const auto metadata_object = config_->metadata_provider_->GetMetadata(upstream_peer); if (metadata_object) { ENVOY_LOG(debug, "Metadata found for upstream peer address {}", upstream_peer->asString()); @@ -349,7 +347,7 @@ void MetadataExchangeFilter::setMetadataNotFoundFilterState() { config_->filter_direction_ == FilterDirection::Downstream ? downstream_peer_address() : upstream_peer_address(); ENVOY_LOG(debug, "Look up metadata based on peer address {}", peer_address->asString()); - const auto metadata_object = config_->metadata_provider_->getMetadata(peer_address); + const auto metadata_object = config_->metadata_provider_->GetMetadata(peer_address); if (metadata_object) { ENVOY_LOG(trace, "Metadata found for peer address {}", peer_address->asString()); updatePeer(metadata_object.value()); @@ -361,10 +359,9 @@ void MetadataExchangeFilter::setMetadataNotFoundFilterState() { } read_callbacks_->connection().streamInfo().filterState()->setData( Istio::Common::NoPeer, std::make_shared(true), - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Connection); + StreamInfo::FilterState::LifeSpan::Connection); } } // namespace MetadataExchange -} // namespace NetworkFilters -} // namespace Extensions +} // namespace Tcp } // namespace Envoy diff --git a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange.h b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange.h index 7a99156f1c7ed..e14bf287e7270 100644 --- a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange.h +++ b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange.h @@ -18,8 +18,7 @@ #include "contrib/istio/filters/common/source/workload_discovery.h" namespace Envoy { -namespace Extensions { -namespace NetworkFilters { +namespace Tcp { namespace MetadataExchange { using ::Envoy::Extensions::Filters::Common::Expr::CelStatePrototype; @@ -98,7 +97,7 @@ class MetadataExchangeFilter : public Network::Filter, public: MetadataExchangeFilter(MetadataExchangeConfigSharedPtr config, const LocalInfo::LocalInfo& local_info) - : config_(config), local_info_(local_info) {} + : config_(config), local_info_(local_info), conn_state_(ConnProtocolNotRead) {} // Network::ReadFilter Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; @@ -155,10 +154,9 @@ class MetadataExchangeFilter : public Network::Filter, NeedMoreDataProxyHeader, // Need more data to be read Done, // Alpn Protocol Found and all the read is done Invalid, // Invalid state, all operations fail - } conn_state_{}; + } conn_state_; }; } // namespace MetadataExchange -} // namespace NetworkFilters -} // namespace Extensions +} // namespace Tcp } // namespace Envoy diff --git a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.cc b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.cc index 449b822ddbaa8..4bb3dec6a5094 100644 --- a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.cc +++ b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.cc @@ -1,13 +1,11 @@ #include "contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.h" namespace Envoy { -namespace Extensions { -namespace NetworkFilters { +namespace Tcp { namespace MetadataExchange { const uint32_t MetadataExchangeInitialHeader::magic_number; } // namespace MetadataExchange -} // namespace NetworkFilters -} // namespace Extensions +} // namespace Tcp } // namespace Envoy diff --git a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.h b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.h index 3362e82af3e55..1e72e63c3cd5f 100644 --- a/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.h +++ b/contrib/istio/filters/network/metadata_exchange/source/metadata_exchange_initial_header.h @@ -5,8 +5,7 @@ #include "envoy/common/platform.h" namespace Envoy { -namespace Extensions { -namespace NetworkFilters { +namespace Tcp { namespace MetadataExchange { // Used with MetadataExchangeHeaderProto to be extensible. @@ -19,6 +18,5 @@ PACKED_STRUCT(struct MetadataExchangeInitialHeader { }); } // namespace MetadataExchange -} // namespace NetworkFilters -} // namespace Extensions +} // namespace Tcp } // namespace Envoy diff --git a/contrib/istio/filters/network/metadata_exchange/test/metadata_exchange_test.cc b/contrib/istio/filters/network/metadata_exchange/test/metadata_exchange_test.cc index b766af5825c06..e82e1fe652b38 100644 --- a/contrib/istio/filters/network/metadata_exchange/test/metadata_exchange_test.cc +++ b/contrib/istio/filters/network/metadata_exchange/test/metadata_exchange_test.cc @@ -17,8 +17,7 @@ using testing::Return; using testing::ReturnRef; namespace Envoy { -namespace Extensions { -namespace NetworkFilters { +namespace Tcp { namespace MetadataExchange { namespace { @@ -140,6 +139,5 @@ TEST_F(MetadataExchangeFilterTest, MetadataExchangeNotFound) { } } // namespace MetadataExchange -} // namespace NetworkFilters -} // namespace Extensions +} // namespace Tcp } // namespace Envoy diff --git a/contrib/istio/filters/network/peer_metadata/source/peer_metadata.cc b/contrib/istio/filters/network/peer_metadata/source/peer_metadata.cc index fc79086144eb4..781e6b39d25b4 100644 --- a/contrib/istio/filters/network/peer_metadata/source/peer_metadata.cc +++ b/contrib/istio/filters/network/peer_metadata/source/peer_metadata.cc @@ -1,5 +1,7 @@ #include "contrib/istio/filters/network/peer_metadata/source/peer_metadata.h" +#include + #include "source/common/router/string_accessor_impl.h" #include "source/common/stream_info/bool_accessor_impl.h" #include "source/common/tcp_proxy/tcp_proxy.h" @@ -15,16 +17,16 @@ using CelState = ::Envoy::Extensions::Filters::Common::Expr::CelState; using CelStateType = ::Envoy::Extensions::Filters::Common::Expr::CelStateType; std::string baggageValue(const LocalInfo::LocalInfo& local_info) { - const auto obj = Istio::Common::convertStructToWorkloadMetadata(local_info.node().metadata()); + const auto obj = ::Istio::Common::convertStructToWorkloadMetadata(local_info.node().metadata()); return obj->baggage(); } -absl::optional internalListenerName(const Network::Address::Instance& address) { +std::optional internalListenerName(const Network::Address::Instance& address) { const auto internal = address.envoyInternalAddress(); if (internal != nullptr) { return internal->addressId(); } - return absl::nullopt; + return std::nullopt; } bool allowedInternalListener(const Network::Address::Instance& address) { @@ -76,7 +78,7 @@ Network::FilterStatus Filter::onWrite(Buffer::Instance& buffer, bool) { // for peer metadata anymore, if the upstream sent it, we'd have it by // now. So we can check if the peer metadata is available or not, and if // no peer metadata available, we can give up waiting for it. - absl::optional peer_metadata = discoverPeerMetadata(); + std::optional peer_metadata = discoverPeerMetadata(); if (peer_metadata) { propagatePeerMetadata(*peer_metadata); } else { @@ -106,7 +108,7 @@ void Filter::populateBaggage() { ASSERT(read_callbacks_); read_callbacks_->connection().streamInfo().filterState()->setData( config_.baggage_key(), std::make_shared(baggage_), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + StreamInfo::FilterState::LifeSpan::FilterChain); } bool Filter::disableDiscovery() const { @@ -115,7 +117,12 @@ bool Filter::disableDiscovery() const { return discoveryDisabled(metadata); } -absl::optional Filter::discoverPeerMetadata() { +// discoveryPeerMetadata is called to check if the baggage HTTP2 CONNECT +// response headers have been populated already in the filter state. +// +// NOTE: It's safe to call this function during any step of processing - it +// will not do anything if the filter is not in the right state. +std::optional Filter::discoverPeerMetadata() { ENVOY_LOG(trace, "Trying to discover peer metadata from filter state set by TCP Proxy"); ASSERT(write_callbacks_); @@ -126,7 +133,7 @@ absl::optional Filter::discoverPeerMetadata() { TcpProxy::TunnelResponseHeaders::key()); if (!state) { ENVOY_LOG(trace, "TCP Proxy didn't set expected filter state"); - return absl::nullopt; + return std::nullopt; } const Http::HeaderMap& headers = state->value(); @@ -135,7 +142,7 @@ absl::optional Filter::discoverPeerMetadata() { ENVOY_LOG( trace, "TCP Proxy saved response headers to the filter state, but there is no baggage header"); - return absl::nullopt; + return std::nullopt; } ENVOY_LOG(trace, @@ -151,21 +158,24 @@ absl::optional Filter::discoverPeerMetadata() { } } - std::unique_ptr metadata = - Istio::Common::convertBaggageToWorkloadMetadata(baggage[0]->value().getStringView(), - identity); + std::unique_ptr<::Istio::Common::WorkloadMetadataObject> metadata = + ::Istio::Common::convertBaggageToWorkloadMetadata(baggage[0]->value().getStringView(), + identity); - Protobuf::Struct data = Istio::Common::convertWorkloadMetadataToStruct(*metadata); - Protobuf::Any wrapped; - wrapped.PackFrom(data); + Envoy::Protobuf::Struct data = ::Istio::Common::convertWorkloadMetadataToStruct(*metadata); + Envoy::Protobuf::Any wrapped; + std::ignore = wrapped.PackFrom(data); return wrapped; } -void Filter::propagatePeerMetadata(const Protobuf::Any& peer_metadata) { +void Filter::propagatePeerMetadata(const Envoy::Protobuf::Any& peer_metadata) { ENVOY_LOG(trace, "Sending peer metadata downstream with the data stream"); ASSERT(write_callbacks_); if (state_ != PeerMetadataState::WaitingForData) { + // It's only safe and correct to send the peer metadata downstream with + // the data if we haven't done that already, otherwise the downstream + // could be very confused by the data they received. ENVOY_LOG(trace, "Filter has already sent the peer metadata downstream"); return; } @@ -203,6 +213,9 @@ Network::FilterStatus UpstreamFilter::onData(Buffer::Instance& buffer, bool end_ if (consumePeerMetadata(buffer, end_stream)) { state_ = PeerMetadataState::PassThrough; } else { + // If we got here it means that we are waiting for more data to arrive. + // NOTE: if error happened, we will not get here, consumePeerMetadata + // will just return true and we will enter PassThrough state. return Network::FilterStatus::StopIteration; } break; @@ -219,6 +232,21 @@ void UpstreamFilter::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks_ = &callbacks; } +// We want to enable baggage based peer metadata discovery if all of the following is true +// - the upstream host is an internal listener, and specifically connect_originate or +// inner_connect_originate internal listener - those are the only listeners that +// support baggage-based peer metadata discovery +// - communication with upstream happens in plain text, e.g., there is no TLS upstream +// transport socket or PROXY transport socket there - we need it in the current +// implemetation of the baggage-based peer metadata discovery because we inject peer +// metadata into the data stream and transport sockets that modify the data stream +// interfere with that (NOTE: in the future release we are planning to lift this +// limitation by communicating over shared memory instead). +// +// We can easily check if the upstream host is an internal listener, so checking the first +// condition is easy. We can't easily check the second condition in the filter itself, so +// instead we rely on istiod providing that information in form of the host metadata on the +// endpoint or cluster level. bool UpstreamFilter::disableDiscovery() const { ASSERT(callbacks_); @@ -251,7 +279,7 @@ bool UpstreamFilter::disableDiscovery() const { bool UpstreamFilter::consumePeerMetadata(Buffer::Instance& buffer, bool end_stream) { ENVOY_LOG(trace, "Trying to consume peer metadata from the data stream"); - using namespace Istio::Common; + using namespace ::Istio::Common; ASSERT(callbacks_); @@ -304,14 +332,14 @@ bool UpstreamFilter::consumePeerMetadata(Buffer::Instance& buffer, bool end_stre absl::string_view data{static_cast(buffer.linearize(peer_metadata_size)), peer_metadata_size}; data = data.substr(sizeof(PeerMetadataHeader)); - Protobuf::Any any; + Envoy::Protobuf::Any any; if (!any.ParseFromArray(data.data(), data.size())) { ENVOY_LOG(trace, "Failed to parse peer metadata proto from the data stream"); populateNoPeerMetadata(); return true; } - Protobuf::Struct peer_metadata; + Envoy::Protobuf::Struct peer_metadata; if (!any.UnpackTo(&peer_metadata)) { ENVOY_LOG(trace, "Failed to unpack peer metadata struct"); populateNoPeerMetadata(); @@ -332,15 +360,23 @@ const CelStatePrototype& UpstreamFilter::peerInfoPrototype() { return *prototype; } -void UpstreamFilter::populatePeerMetadata(const Istio::Common::WorkloadMetadataObject& peer) { +void UpstreamFilter::populatePeerMetadata(const ::Istio::Common::WorkloadMetadataObject& peer) { ENVOY_LOG(trace, "Populating peer metadata in the upstream filter state"); ASSERT(callbacks_); - auto proto = Istio::Common::convertWorkloadMetadataToStruct(peer); + auto proto = ::Istio::Common::convertWorkloadMetadataToStruct(peer); auto cel = std::make_shared(peerInfoPrototype()); cel->setValue(absl::string_view(proto.SerializeAsString())); callbacks_->connection().streamInfo().filterState()->setData( - Istio::Common::UpstreamPeer, std::move(cel), StreamInfo::FilterState::StateType::ReadOnly, + ::Istio::Common::UpstreamPeer, std::move(cel), StreamInfo::FilterState::LifeSpan::Connection); + // Also store the WorkloadMetadataObject under the *_obj key, mirroring the + // metadata_exchange network filter. istio.stats reads the object directly + // (peerInfoRead/peerInfo prefer it); its CelState fallback uses BaggageToken + // keys that are incompatible with convertWorkloadMetadataToStruct's output, so + // without this the peer identity is lost over HBONE. + callbacks_->connection().streamInfo().filterState()->setData( + ::Istio::Common::UpstreamPeerObj, + std::make_shared<::Istio::Common::WorkloadMetadataObject>(peer), StreamInfo::FilterState::LifeSpan::Connection); } @@ -349,8 +385,8 @@ void UpstreamFilter::populateNoPeerMetadata() { ASSERT(callbacks_); callbacks_->connection().streamInfo().filterState()->setData( - Istio::Common::NoPeer, std::make_shared(true), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::Connection); + ::Istio::Common::NoPeer, std::make_shared(true), + StreamInfo::FilterState::LifeSpan::Connection); } ConfigFactory::ConfigFactory() diff --git a/contrib/istio/filters/network/peer_metadata/source/peer_metadata.h b/contrib/istio/filters/network/peer_metadata/source/peer_metadata.h index 642ba2ac4f7e5..6c136e8caec31 100644 --- a/contrib/istio/filters/network/peer_metadata/source/peer_metadata.h +++ b/contrib/istio/filters/network/peer_metadata/source/peer_metadata.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/local_info/local_info.h" @@ -15,13 +16,81 @@ #include "contrib/envoy/extensions/filters/network/peer_metadata/v3/peer_metadata.pb.validate.h" #include "contrib/istio/filters/common/source/metadata_object.h" +/** + * PeerMetadata network and upstream network filters are used in one of ambient + * peer metadata discovery mechanims. The peer metadata discovery mechanism + * these filters are part of relies on peer reporting their own metadata in + * HBONE CONNECT request and response headers. + * + * The purpose of these filters is to extract this metadata from the request/ + * response headers and propagate it to the Istio filters reporting telemetry + * where this metadata will be used as labels. + * + * The filters in this folder are specifically concerned with extracting and + * propagating upstream peer metadata. The working setup includes a combination + * of several filters that together get the job done. + * + * A bit of background, here is a very simplified description of how Istio + * waypoint processes a request: + * + * 1. connect_terminate listener recieves an incoming HBONE connection; + * * it uwraps HBONE tunnel and extracts the data passed inside it; + * * it passes the data inside the HBONE tunnel to a main_internal listener + * that performs the next stage of processing; + * 2. main_internal listener is responsible for parsing the data as L7 data + * (HTTP/gRPC), applying configured L7 policies, picking the endpoint to + * route the request to and reports L7 stats + * * At this level we are processing the incoming request at L7 level and + * have access to things like status of the request and can report + * meaningful metrics; + * * To report in metrics where the request came from and where it went + * after we need to know the details of downstream and upstream peers - + * that's what we call peer metadata; + * * Once we've done with L7 processing of the request, we pass the request + * to the connect_originate (or inner_connect_originate in case of double + * HBONE) listener that will handle the next stage of processing; + * 3. connect_originate - is responsible for wrapping processed L7 traffic into + * an HBONE tunnel and sending it out + * * This stage of processing treats data as a stream of bytes without any + * knowledge of L7 protocol details; + * * It takes the upstream peer address as input an establishes an HBONE + * tunnel to the destination and sends the data via that tunnel. + * + * With that picture in mind, what we want to do is in connect_originate (or + * inner_connect_originate in case of double-HBONE) when we establish HBONE + * tunnel, we want to extract peer metadata from the CONNECT response and + * propagate it to the main_internal. + * + * To establish HBONE tunnel we rely on Envoy TCP Proxy filter, so we don't + * handle HTTP2 CONNECT responses or requests directly, instead we rely on the + * TCP Proxy filter to extract required information from the response and save + * it in the filter state. We then use the custom network filter to take filter + * state proved by TCP Proxy filter, encode it, and send it to main_internal + * *as data* before any actual response data. This is what the network filter + * defined here is responsible for. + * + * In main_internal we use a custom upstream network filter to extract and + * remove the metadata from the data stream and populate filter state that + * could be used by Istio telemetry filters. That's what the upstream network + * filter defined here is responsible for. + * + * Why do we do it this way? Generally in Envoy we use filter state and dynamic + * metadata to communicate additional information between filters. While it's + * possible to propagate filter state from downstream to upstream, i.e., we + * could set filter state in connect_terminate and propagate it to + * main_internal and then to connect_originate, it's not possible to propagate + * filter state from upstream to downstream, i.e., we cannot make filter state + * set in connect_originate available to main_internal directly. That's why we + * push that metadata with the data instead. + */ + namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace PeerMetadata { -using Config = ::envoy::extensions::filters::network::peer_metadata::v3::Config; -using UpstreamConfig = ::envoy::extensions::filters::network::peer_metadata::v3::UpstreamConfig; +using Config = ::envoy::extensions::network_filters::peer_metadata::Config; +using UpstreamConfig = ::envoy::extensions::network_filters::peer_metadata::UpstreamConfig; using CelStatePrototype = ::Envoy::Extensions::Filters::Common::Expr::CelStatePrototype; struct HeaderValues { @@ -72,8 +141,8 @@ class Filter : public Network::Filter, Logger::Loggable { private: void populateBaggage(); bool disableDiscovery() const; - absl::optional discoverPeerMetadata(); - void propagatePeerMetadata(const Protobuf::Any& peer_metadata); + std::optional discoverPeerMetadata(); + void propagatePeerMetadata(const Envoy::Protobuf::Any& peer_metadata); void propagateNoPeerMetadata(); PeerMetadataState state_ = PeerMetadataState::WaitingForData; @@ -89,6 +158,15 @@ class Filter : public Network::Filter, Logger::Loggable { * HBONE) to communicate with the upstream peers and it will parse and remove * the data injected by the filter above. The parsed peer metadata details will * be saved in the filter state. + * + * NOTE: This filter has built-in safety checks that would prevent it from + * trying to interpret the actual connection data as peer metadata injected + * by the filter above. However, those checks are rather shallow and rely on a + * bunch of implicit assumptions (i.e., the magic number does not match + * accidentally, the upstream host actually sends back some data that we can + * check, etc). What I'm trying to say is that in correct setup we don't need + * to rely on those checks for correctness and if it's not the case, then we + * definitely have a bug. */ class UpstreamFilter : public Network::ReadFilter, Logger::Loggable { public: @@ -105,7 +183,7 @@ class UpstreamFilter : public Network::ReadFilter, Logger::Loggable { public: @@ -127,6 +209,11 @@ class ConfigFactory : public Common::ExceptionFreeFactoryBase { /** * PeerMetadata upstream network filter factory. + * + * This filter is responsible for detecting the peer metadata passed in the + * data stream, parsing it, populating filter state based on that and finally + * removing it from the data stream, so that downstream filters can process + * the data as usual. */ class UpstreamConfigFactory : public Server::Configuration::NamedUpstreamNetworkFilterConfigFactory { diff --git a/contrib/istio/filters/network/peer_metadata/test/peer_metadata_test.cc b/contrib/istio/filters/network/peer_metadata/test/peer_metadata_test.cc index 703fb157226e1..93c7360dfeb58 100644 --- a/contrib/istio/filters/network/peer_metadata/test/peer_metadata_test.cc +++ b/contrib/istio/filters/network/peer_metadata/test/peer_metadata_test.cc @@ -13,6 +13,7 @@ * limitations under the License. */ #include +#include #include #include @@ -92,7 +93,6 @@ class PeerMetadataFilterTest : public ::testing::Test { std::make_unique(response_headers)); auto filter_state = stream_info_.filterState(); filter_state->setData(TcpProxy::TunnelResponseHeaders::key(), headers, - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Connection); } @@ -315,9 +315,9 @@ std::string encodePeerMetadataHeaderOnly(const PeerMetadataHeader& header) { std::string encodeMetadataOnly(absl::string_view baggage, absl::string_view identity) { std::unique_ptr metadata = Istio::Common::convertBaggageToWorkloadMetadata(baggage, identity); - Protobuf::Struct data = convertWorkloadMetadataToStruct(*metadata); + Protobuf::Struct data = ::Istio::Common::convertWorkloadMetadataToStruct(*metadata); Protobuf::Any wrapped; - wrapped.PackFrom(data); + std::ignore = wrapped.PackFrom(data); return wrapped.SerializeAsString(); } @@ -346,24 +346,24 @@ class PeerMetadataUpstreamFilterTest : public ::testing::Test { return upstream_host_->cluster_.metadata_; } - absl::optional peerInfoFromFilterState() const { + std::optional peerInfoFromFilterState() const { const auto& filter_state = stream_info_.filterState(); const auto* cel_state = filter_state.getDataReadOnly( Istio::Common::UpstreamPeer); if (!cel_state) { - return absl::nullopt; + return std::nullopt; } Protobuf::Struct obj; if (!obj.ParseFromString(absl::string_view(cel_state->value()))) { - return absl::nullopt; + return std::nullopt; } std::unique_ptr peer_info = Istio::Common::convertStructToWorkloadMetadata(obj); if (!peer_info) { - return absl::nullopt; + return std::nullopt; } return *peer_info; diff --git a/contrib/kae/private_key_providers/source/config.cc b/contrib/kae/private_key_providers/source/config.cc index 9be51343a5137..49bfb044cf351 100644 --- a/contrib/kae/private_key_providers/source/config.cc +++ b/contrib/kae/private_key_providers/source/config.cc @@ -37,6 +37,7 @@ KaePrivateKeyMethodFactory::createPrivateKeyMethodProviderInstance( *message, private_key_provider_context.messageValidationVisitor()); #ifdef KAE_DISABLED + static_cast(conf); throw EnvoyException("Arm64 architecture is required for KAE."); #else LibUadkCryptoSharedPtr libuadk = std::make_shared(); diff --git a/contrib/kafka/filters/network/source/BUILD b/contrib/kafka/filters/network/source/BUILD index 80477fffabfd4..8b407b1d8c3d6 100644 --- a/contrib/kafka/filters/network/source/BUILD +++ b/contrib/kafka/filters/network/source/BUILD @@ -257,6 +257,5 @@ envoy_cc_library( ], deps = [ "//source/common/common:macros", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/contrib/kafka/filters/network/source/broker/filter_config.cc b/contrib/kafka/filters/network/source/broker/filter_config.cc index 47c9c247f130f..5520d7fcae6f9 100644 --- a/contrib/kafka/filters/network/source/broker/filter_config.cc +++ b/contrib/kafka/filters/network/source/broker/filter_config.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/broker/filter_config.h" +#include + #include "source/common/common/assert.h" namespace Envoy { @@ -49,7 +51,7 @@ bool BrokerFilterConfig::needsResponseRewrite() const { return force_response_rewrite_ || !broker_address_rewrite_rules_.empty(); } -absl::optional +std::optional BrokerFilterConfig::findBrokerAddressOverride(const uint32_t broker_id) const { for (const auto& rule : broker_address_rewrite_rules_) { if (rule.matches(broker_id)) { @@ -57,7 +59,7 @@ BrokerFilterConfig::findBrokerAddressOverride(const uint32_t broker_id) const { return {hp}; } } - return absl::nullopt; + return std::nullopt; } absl::flat_hash_set BrokerFilterConfig::apiKeysAllowed() const { diff --git a/contrib/kafka/filters/network/source/broker/filter_config.h b/contrib/kafka/filters/network/source/broker/filter_config.h index 51ff95dc9193e..c0eeea33fab0e 100644 --- a/contrib/kafka/filters/network/source/broker/filter_config.h +++ b/contrib/kafka/filters/network/source/broker/filter_config.h @@ -1,9 +1,9 @@ #pragma once +#include #include #include "absl/container/flat_hash_set.h" -#include "absl/types/optional.h" #include "contrib/envoy/extensions/filters/network/kafka_broker/v3/kafka_broker.pb.h" #include "contrib/envoy/extensions/filters/network/kafka_broker/v3/kafka_broker.pb.validate.h" @@ -57,7 +57,7 @@ class BrokerFilterConfig { /** * Returns override address for a broker. */ - virtual absl::optional findBrokerAddressOverride(const uint32_t broker_id) const; + virtual std::optional findBrokerAddressOverride(const uint32_t broker_id) const; /** * Returns what API keys are allowed. diff --git a/contrib/kafka/filters/network/source/broker/rewriter.h b/contrib/kafka/filters/network/source/broker/rewriter.h index fb8e247058ff0..d20edc50eb04f 100644 --- a/contrib/kafka/filters/network/source/broker/rewriter.h +++ b/contrib/kafka/filters/network/source/broker/rewriter.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/buffer/buffer.h" @@ -69,7 +70,7 @@ class ResponseRewriterImpl : public ResponseRewriter, private Logger::Loggable void maybeUpdateHostAndPort(T& arg, const int32_t T::*node_id_field) const { const int32_t node_id = arg.*node_id_field; - const absl::optional hostAndPort = config_->findBrokerAddressOverride(node_id); + const std::optional hostAndPort = config_->findBrokerAddressOverride(node_id); if (hostAndPort) { ENVOY_LOG(trace, "Changing broker [{}] from {}:{} to {}:{}", node_id, arg.host_, arg.port_, hostAndPort->first, hostAndPort->second); diff --git a/contrib/kafka/filters/network/source/kafka_request_parser.h b/contrib/kafka/filters/network/source/kafka_request_parser.h index a76067d7ebe47..387c4e5d632ef 100644 --- a/contrib/kafka/filters/network/source/kafka_request_parser.h +++ b/contrib/kafka/filters/network/source/kafka_request_parser.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/common/exception.h" @@ -32,7 +33,7 @@ struct RequestContext { /** * Request header that gets filled in during the parse. */ - RequestHeader request_header_{-1, -1, -1, absl::nullopt}; + RequestHeader request_header_{-1, -1, -1, std::nullopt}; /** * Bytes left to consume. diff --git a/contrib/kafka/filters/network/source/kafka_types.h b/contrib/kafka/filters/network/source/kafka_types.h index 36ce3b430b83b..8a73534cb7883 100644 --- a/contrib/kafka/filters/network/source/kafka_types.h +++ b/contrib/kafka/filters/network/source/kafka_types.h @@ -1,12 +1,11 @@ #pragma once #include +#include #include #include #include -#include "absl/types/optional.h" - namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -15,7 +14,7 @@ namespace Kafka { /** * Nullable string used by Kafka. */ -using NullableString = absl::optional; +using NullableString = std::optional; /** * Bytes array used by Kafka. @@ -25,12 +24,12 @@ using Bytes = std::vector; /** * Nullable bytes array used by Kafka. */ -using NullableBytes = absl::optional; +using NullableBytes = std::optional; /** * Kafka array of elements of type T. */ -template using NullableArray = absl::optional>; +template using NullableArray = std::optional>; /** * Analogous to: diff --git a/contrib/kafka/filters/network/source/mesh/command_handlers/fetch_record_converter.cc b/contrib/kafka/filters/network/source/mesh/command_handlers/fetch_record_converter.cc index 5a7e92cc2251d..01786812dd71b 100644 --- a/contrib/kafka/filters/network/source/mesh/command_handlers/fetch_record_converter.cc +++ b/contrib/kafka/filters/network/source/mesh/command_handlers/fetch_record_converter.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/command_handlers/fetch_record_converter.h" +#include + #include "contrib/kafka/filters/network/source/serialization.h" namespace Envoy { @@ -34,7 +36,7 @@ FetchRecordConverterImpl::convert(const InboundRecordsMap& arg) const { const int16_t error_code = 0; const int64_t high_watermark = 0; const auto frrpd = FetchResponseResponsePartitionData{partition, error_code, high_watermark, - absl::make_optional(record_batch.second)}; + std::make_optional(record_batch.second)}; frrpds.push_back(frrpd); } diff --git a/contrib/kafka/filters/network/source/mesh/command_handlers/metadata.cc b/contrib/kafka/filters/network/source/mesh/command_handlers/metadata.cc index 07f402a80802e..fd351b27b7bb5 100644 --- a/contrib/kafka/filters/network/source/mesh/command_handlers/metadata.cc +++ b/contrib/kafka/filters/network/source/mesh/command_handlers/metadata.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/command_handlers/metadata.h" +#include + #include "contrib/kafka/filters/network/source/external/responses.h" namespace Envoy { @@ -42,7 +44,7 @@ AbstractResponseSharedPtr MetadataRequestHolder::computeAnswer() const { } const std::string& topic_name = *(topic.name_); std::vector topic_partitions; - const absl::optional cluster_config = + const std::optional cluster_config = configuration_.computeClusterConfigForTopic(topic_name); if (!cluster_config) { // Someone is requesting topics that are not known to our configuration. diff --git a/contrib/kafka/filters/network/source/mesh/shared_consumer_manager_impl.cc b/contrib/kafka/filters/network/source/mesh/shared_consumer_manager_impl.cc index 40a0e5894e4f2..062b919bc2474 100644 --- a/contrib/kafka/filters/network/source/mesh/shared_consumer_manager_impl.cc +++ b/contrib/kafka/filters/network/source/mesh/shared_consumer_manager_impl.cc @@ -1,6 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/shared_consumer_manager_impl.h" #include +#include #include "source/common/common/fmt.h" @@ -84,7 +85,7 @@ void SharedConsumerManagerImpl::registerNewConsumer(const std::string& topic) { ENVOY_LOG(debug, "Creating consumer for topic [{}]", topic); // Compute which upstream cluster corresponds to the topic. - const absl::optional cluster_config = + const std::optional cluster_config = configuration_.computeClusterConfigForTopic(topic); if (!cluster_config) { throw EnvoyException( @@ -315,7 +316,7 @@ int32_t countForTest(const std::string& topic, const int32_t partition, Partitio if (map.end() != it) { return it->second.size(); } else { - return -1; // Tests are simpler to type if we do this instead of absl::optional. + return -1; // Tests are simpler to type if we do this instead of std::optional. } } diff --git a/contrib/kafka/filters/network/source/mesh/upstream_config.cc b/contrib/kafka/filters/network/source/mesh/upstream_config.cc index 163e30ef308e4..1c541638991f5 100644 --- a/contrib/kafka/filters/network/source/mesh/upstream_config.cc +++ b/contrib/kafka/filters/network/source/mesh/upstream_config.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/upstream_config.h" +#include + #include "envoy/common/exception.h" #include "source/common/common/assert.h" @@ -88,16 +90,16 @@ UpstreamKafkaConfigurationImpl::UpstreamKafkaConfigurationImpl(const KafkaMeshPr ASSERT(config.consumer_proxy_mode() == KafkaMesh::StatefulConsumerProxy); } -absl::optional +std::optional UpstreamKafkaConfigurationImpl::computeClusterConfigForTopic(const std::string& topic) const { // We find the first matching prefix (this is why ordering is important). for (const auto& it : topic_prefix_to_cluster_config_) { if (absl::StartsWith(topic, it.first)) { const ClusterConfig cluster_config = it.second; - return absl::make_optional(cluster_config); + return std::make_optional(cluster_config); } } - return absl::nullopt; + return std::nullopt; } std::pair UpstreamKafkaConfigurationImpl::getAdvertisedAddress() const { diff --git a/contrib/kafka/filters/network/source/mesh/upstream_config.h b/contrib/kafka/filters/network/source/mesh/upstream_config.h index a8b14084ce9d7..831ef241a4f31 100644 --- a/contrib/kafka/filters/network/source/mesh/upstream_config.h +++ b/contrib/kafka/filters/network/source/mesh/upstream_config.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -9,7 +10,6 @@ #include "source/common/common/logger.h" -#include "absl/types/optional.h" #include "contrib/envoy/extensions/filters/network/kafka_mesh/v3alpha/kafka_mesh.pb.h" #include "contrib/envoy/extensions/filters/network/kafka_mesh/v3alpha/kafka_mesh.pb.validate.h" @@ -63,7 +63,7 @@ class UpstreamKafkaConfiguration { // Provides cluster for given Kafka topic, according to the rules contained within this // configuration object. - virtual absl::optional + virtual std::optional computeClusterConfigForTopic(const std::string& topic) const PURE; }; @@ -78,7 +78,7 @@ class UpstreamKafkaConfigurationImpl : public UpstreamKafkaConfiguration, UpstreamKafkaConfigurationImpl(const KafkaMeshProtoConfig& config); // UpstreamKafkaConfiguration - absl::optional + std::optional computeClusterConfigForTopic(const std::string& topic) const override; // UpstreamKafkaConfiguration diff --git a/contrib/kafka/filters/network/source/mesh/upstream_kafka_consumer_impl.cc b/contrib/kafka/filters/network/source/mesh/upstream_kafka_consumer_impl.cc index 88db6e7500432..df0bea7814fe4 100644 --- a/contrib/kafka/filters/network/source/mesh/upstream_kafka_consumer_impl.cc +++ b/contrib/kafka/filters/network/source/mesh/upstream_kafka_consumer_impl.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/upstream_kafka_consumer_impl.h" +#include + #include "contrib/kafka/filters/network/source/kafka_types.h" #include "contrib/kafka/filters/network/source/mesh/librdkafka_utils_impl.h" @@ -65,7 +67,7 @@ RichKafkaConsumer::~RichKafkaConsumer() { consumer_->unassign(); consumer_->close(); - ENVOY_LOG(debug, "Kafka consumer [{}] closed succesfully", topic_); + ENVOY_LOG(debug, "Kafka consumer [{}] closed successfully", topic_); } // Read timeout constants. @@ -107,7 +109,7 @@ static NullableBytes toBytes(const void* data, const size_t size) { Bytes bytes(as_char, as_char + size); return {bytes}; } else { - return absl::nullopt; + return std::nullopt; } } diff --git a/contrib/kafka/filters/network/source/mesh/upstream_kafka_facade.cc b/contrib/kafka/filters/network/source/mesh/upstream_kafka_facade.cc index 6d096a0c95255..aacd5718acac7 100644 --- a/contrib/kafka/filters/network/source/mesh/upstream_kafka_facade.cc +++ b/contrib/kafka/filters/network/source/mesh/upstream_kafka_facade.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/mesh/upstream_kafka_facade.h" +#include + #include "contrib/kafka/filters/network/source/mesh/upstream_kafka_client_impl.h" namespace Envoy { @@ -47,7 +49,7 @@ ThreadLocalKafkaFacade::~ThreadLocalKafkaFacade() { } KafkaProducer& ThreadLocalKafkaFacade::getProducerForTopic(const std::string& topic) { - const absl::optional cluster_config = + const std::optional cluster_config = configuration_.computeClusterConfigForTopic(topic); if (cluster_config) { const auto it = cluster_to_kafka_client_.find(cluster_config->name_); diff --git a/contrib/kafka/filters/network/source/protocol/generator.py b/contrib/kafka/filters/network/source/protocol/generator.py index 9e6600ff5ef46..b8c138ebc23fc 100755 --- a/contrib/kafka/filters/network/source/protocol/generator.py +++ b/contrib/kafka/filters/network/source/protocol/generator.py @@ -358,7 +358,7 @@ def constructor_init_list(self): init_list.append(init_list_item) else: # Field is optional, and the parameter is T in this version. - init_list_item = '%s_{absl::make_optional(%s)}' % (field.name, field.name) + init_list_item = '%s_{std::make_optional(%s)}' % (field.name, field.name) init_list.append(init_list_item) else: # Field is T, so parameter cannot be optional. @@ -408,13 +408,13 @@ def used_in_version(self, version): def field_declaration(self): if self.is_nullable(): - return 'absl::optional<%s> %s' % (self.type.name, self.name) + return 'std::optional<%s> %s' % (self.type.name, self.name) else: return '%s %s' % (self.type.name, self.name) def parameter_declaration(self, version): if self.is_nullable_in_version(version): - return 'absl::optional<%s> %s' % (self.type.name, self.name) + return 'std::optional<%s> %s' % (self.type.name, self.name) else: return '%s %s' % (self.type.name, self.name) @@ -425,13 +425,13 @@ def default_value(self): if type_default_value != 'null': return '{%s}' % type_default_value else: - return 'absl::nullopt' + return 'std::nullopt' else: return str(self.type.default_value()) def example_value_for_test(self, version): if self.is_nullable_in_version(version): - return 'absl::make_optional<%s>(%s)' % ( + return 'std::make_optional<%s>(%s)' % ( self.type.name, self.type.example_value_for_test(version)) else: return str(self.type.example_value_for_test(version)) diff --git a/contrib/kafka/filters/network/source/serialization.cc b/contrib/kafka/filters/network/source/serialization.cc index 45f2229d8892f..3bde43c8b6979 100644 --- a/contrib/kafka/filters/network/source/serialization.cc +++ b/contrib/kafka/filters/network/source/serialization.cc @@ -1,5 +1,7 @@ #include "contrib/kafka/filters/network/source/serialization.h" +#include + namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -193,9 +195,9 @@ uint32_t NullableCompactStringDeserializer::feed(absl::string_view& data) { NullableString NullableCompactStringDeserializer::get() const { const uint32_t original_data_len = length_buf_.get(); if (NULL_COMPACT_STRING_LENGTH == original_data_len) { - return absl::nullopt; + return std::nullopt; } else { - return absl::make_optional(std::string(data_buf_.begin(), data_buf_.end())); + return std::make_optional(std::string(data_buf_.begin(), data_buf_.end())); } } @@ -214,9 +216,9 @@ uint32_t NullableCompactBytesDeserializer::feed(absl::string_view& data) { NullableBytes NullableCompactBytesDeserializer::get() const { const uint32_t original_data_len = length_buf_.get(); if (NULL_COMPACT_BYTES_LENGTH == original_data_len) { - return absl::nullopt; + return std::nullopt; } else { - return absl::make_optional(data_buf_); + return std::make_optional(data_buf_); } } diff --git a/contrib/kafka/filters/network/source/serialization.h b/contrib/kafka/filters/network/source/serialization.h index aa805f21d73ce..94d4f82d68540 100644 --- a/contrib/kafka/filters/network/source/serialization.h +++ b/contrib/kafka/filters/network/source/serialization.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -423,8 +424,8 @@ class NullableStringDeserializer : public Deserializer { bool ready() const override { return ready_; } NullableString get() const override { - return required_ >= 0 ? absl::make_optional(std::string(data_buf_.begin(), data_buf_.end())) - : absl::nullopt; + return required_ >= 0 ? std::make_optional(std::string(data_buf_.begin(), data_buf_.end())) + : std::nullopt; } private: @@ -540,7 +541,7 @@ class NullableBytesDeserializer : public Deserializer { bool ready() const override { return ready_; } NullableBytes get() const override { - return required_ >= 0 ? absl::make_optional(data_buf_) : absl::nullopt; + return required_ >= 0 ? std::make_optional(data_buf_) : std::nullopt; } private: @@ -816,7 +817,7 @@ class NullableArrayDeserializer } return result; } else { - return absl::nullopt; + return std::nullopt; } } @@ -904,7 +905,7 @@ class NullableCompactArrayDeserializer } return result; } else { - return absl::nullopt; + return std::nullopt; } } @@ -924,9 +925,9 @@ class NullableCompactArrayDeserializer */ template class NullableStructDeserializer - : public Deserializer> { + : public Deserializer> { public: - using ResponseType = absl::optional; + using ResponseType = std::optional; uint32_t feed(absl::string_view& data) override { @@ -945,7 +946,7 @@ class NullableStructDeserializer marker_consumed_ = true; if (marker >= 0) { - data_buf_ = absl::make_optional(DeserializerType()); + data_buf_ = std::make_optional(DeserializerType()); } else { return bytes_read; } @@ -973,15 +974,15 @@ class NullableStructDeserializer ResponseType get() const override { if (data_buf_) { const typename ResponseType::value_type deserialized_form = data_buf_->get(); - return absl::make_optional(deserialized_form); + return std::make_optional(deserialized_form); } else { - return absl::nullopt; + return std::nullopt; } } private: bool marker_consumed_{false}; - absl::optional data_buf_; // Present if marker was consumed and was 0 or more. + std::optional data_buf_; // Present if marker was consumed and was 0 or more. }; /** @@ -1066,7 +1067,7 @@ class EncodingContext { * Compute size of given nullable object, if it were to be encoded. * @return serialized size of argument. */ - template uint32_t computeSize(const absl::optional& arg) const; + template uint32_t computeSize(const std::optional& arg) const; /** * Compute size of given reference, if it were to be compactly encoded. @@ -1108,7 +1109,7 @@ class EncodingContext { * Encode given nullable object in a buffer. * @return bytes written */ - template uint32_t encode(const absl::optional& arg, Buffer::Instance& dst); + template uint32_t encode(const std::optional& arg, Buffer::Instance& dst); /** * Compactly encode given reference in a buffer. @@ -1218,7 +1219,7 @@ inline uint32_t EncodingContext::computeSize(const NullableArray& arg) const * The size of nullable object is 1 (for market byte) and the size of real object (if any). */ template -inline uint32_t EncodingContext::computeSize(const absl::optional& arg) const { +inline uint32_t EncodingContext::computeSize(const std::optional& arg) const { return 1 + (arg ? computeSize(*arg) : 0); } @@ -1523,7 +1524,7 @@ uint32_t EncodingContext::encode(const NullableArray& arg, Buffer::Instance& * have it to serialize itself. */ template -uint32_t EncodingContext::encode(const absl::optional& arg, Buffer::Instance& dst) { +uint32_t EncodingContext::encode(const std::optional& arg, Buffer::Instance& dst) { if (arg) { const int8_t marker = 1; encode(marker, dst); diff --git a/contrib/kafka/filters/network/source/tagged_fields.h b/contrib/kafka/filters/network/source/tagged_fields.h index 96e3c72e50bba..ffbe4e6f71351 100644 --- a/contrib/kafka/filters/network/source/tagged_fields.h +++ b/contrib/kafka/filters/network/source/tagged_fields.h @@ -50,6 +50,9 @@ struct TaggedField { */ class TaggedFieldDeserializer : public Deserializer { public: + // protects against memory exhaustion via attacker controlled field data length. + static constexpr uint32_t MAX_TAGGED_FIELD_DATA_SIZE = 64 * 1024; // 64 kb + TaggedFieldDeserializer() = default; uint32_t feed(absl::string_view& data) override { @@ -63,14 +66,17 @@ class TaggedFieldDeserializer : public Deserializer { if (!length_consumed_) { required_ = length_deserializer_.get(); - data_buffer_ = std::vector(required_); + if (required_ > MAX_TAGGED_FIELD_DATA_SIZE) { + throw EnvoyException(fmt::format("Tagged field data length {} exceeds maximum allowed {}", + required_, MAX_TAGGED_FIELD_DATA_SIZE)); + } + data_buffer_.reserve(required_); length_consumed_ = true; } const uint32_t data_consumed = std::min(required_, data.size()); - const uint32_t written = data_buffer_.size() - required_; if (data_consumed > 0) { - memcpy(data_buffer_.data() + written, data.data(), data_consumed); // NOLINT(safe-memcpy) + data_buffer_.insert(data_buffer_.end(), data.data(), data.data() + data_consumed); required_ -= data_consumed; data = {data.data() + data_consumed, data.size() - data_consumed}; } @@ -128,6 +134,9 @@ struct TaggedFields { */ class TaggedFieldsDeserializer : public Deserializer { public: + // protects against memory exhaustion via attacker controlled tagged field count. + static constexpr uint32_t MAX_TAGGED_FIELD_COUNT = 64; + uint32_t feed(absl::string_view& data) override { const uint32_t count_consumed = count_deserializer_.feed(data); @@ -137,6 +146,10 @@ class TaggedFieldsDeserializer : public Deserializer { if (!children_setup_) { const uint32_t field_count = count_deserializer_.get(); + if (field_count > MAX_TAGGED_FIELD_COUNT) { + throw EnvoyException(fmt::format("Tagged field count {} exceeds maximum allowed {}", + field_count, MAX_TAGGED_FIELD_COUNT)); + } children_ = std::vector(field_count); children_setup_ = true; } diff --git a/contrib/kafka/filters/network/test/broker/mock_filter_config.h b/contrib/kafka/filters/network/test/broker/mock_filter_config.h index e39614a6cd30a..d676ecdc8c898 100644 --- a/contrib/kafka/filters/network/test/broker/mock_filter_config.h +++ b/contrib/kafka/filters/network/test/broker/mock_filter_config.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "contrib/kafka/filters/network/source/broker/filter_config.h" #include "gmock/gmock.h" @@ -13,7 +15,7 @@ class MockBrokerFilterConfig : public BrokerFilterConfig { public: MOCK_METHOD((const std::string&), stat_prefix, (), (const)); MOCK_METHOD(bool, needsResponseRewrite, (), (const)); - MOCK_METHOD((absl::optional), findBrokerAddressOverride, (const uint32_t), (const)); + MOCK_METHOD((std::optional), findBrokerAddressOverride, (const uint32_t), (const)); MOCK_METHOD((absl::flat_hash_set), apiKeysAllowed, (), (const)); MOCK_METHOD((absl::flat_hash_set), apiKeysDenied, (), (const)); MockBrokerFilterConfig() : BrokerFilterConfig{"prefix", false, {}, {}, {}} {}; diff --git a/contrib/kafka/filters/network/test/broker/rewriter_unit_test.cc b/contrib/kafka/filters/network/test/broker/rewriter_unit_test.cc index 6020b5ea53982..b87f0f5444506 100644 --- a/contrib/kafka/filters/network/test/broker/rewriter_unit_test.cc +++ b/contrib/kafka/filters/network/test/broker/rewriter_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "source/common/buffer/buffer_impl.h" #include "contrib/kafka/filters/network/source/broker/filter_config.h" @@ -81,11 +83,11 @@ TEST(ResponseRewriterImplUnitTest, ShouldRewriteMetadataResponse) { MetadataResponse mr = {brokers, {}}; auto config = std::make_shared(); - absl::optional r1 = {{"nh1", 4444}}; + std::optional r1 = {{"nh1", 4444}}; EXPECT_CALL(*config, findBrokerAddressOverride(b1.node_id_)).WillOnce(Return(r1)); - absl::optional r2 = absl::nullopt; + std::optional r2 = std::nullopt; EXPECT_CALL(*config, findBrokerAddressOverride(b2.node_id_)).WillOnce(Return(r2)); - absl::optional r3 = {{"nh3", 6666}}; + std::optional r3 = {{"nh3", 6666}}; EXPECT_CALL(*config, findBrokerAddressOverride(b3.node_id_)).WillOnce(Return(r3)); ResponseRewriterImpl testee{config}; @@ -108,13 +110,13 @@ TEST(ResponseRewriterImplUnitTest, ShouldRewriteFindCoordinatorResponse) { fcr.coordinators_ = {c1, c2, c3}; auto config = std::make_shared(); - absl::optional fcrhp = {{"nh1", 4444}}; + std::optional fcrhp = {{"nh1", 4444}}; EXPECT_CALL(*config, findBrokerAddressOverride(fcr.node_id_)).WillOnce(Return(fcrhp)); - absl::optional cr1 = {{"nh1", 4444}}; + std::optional cr1 = {{"nh1", 4444}}; EXPECT_CALL(*config, findBrokerAddressOverride(c1.node_id_)).WillOnce(Return(cr1)); - absl::optional cr2 = absl::nullopt; + std::optional cr2 = std::nullopt; EXPECT_CALL(*config, findBrokerAddressOverride(c2.node_id_)).WillOnce(Return(cr2)); - absl::optional cr3 = {{"nh3", 6666}}; + std::optional cr3 = {{"nh3", 6666}}; EXPECT_CALL(*config, findBrokerAddressOverride(c3.node_id_)).WillOnce(Return(cr3)); ResponseRewriterImpl testee{config}; @@ -130,18 +132,18 @@ TEST(ResponseRewriterImplUnitTest, ShouldRewriteFindCoordinatorResponse) { TEST(ResponseRewriterImplUnitTest, ShouldRewriteDescribeClusterResponse) { // given - DescribeClusterBroker b1 = {13, "host1", 1111, absl::nullopt, {}}; - DescribeClusterBroker b2 = {42, "host2", 2222, absl::nullopt, {}}; - DescribeClusterBroker b3 = {77, "host3", 3333, absl::nullopt, {}}; + DescribeClusterBroker b1 = {13, "host1", 1111, std::nullopt, {}}; + DescribeClusterBroker b2 = {42, "host2", 2222, std::nullopt, {}}; + DescribeClusterBroker b3 = {77, "host3", 3333, std::nullopt, {}}; std::vector brokers = {b1, b2, b3}; - DescribeClusterResponse dcr = {0, 0, absl::nullopt, "", 0, brokers, 0, {}}; + DescribeClusterResponse dcr = {0, 0, std::nullopt, "", 0, brokers, 0, {}}; auto config = std::make_shared(); - absl::optional cr1 = {{"nh1", 4444}}; + std::optional cr1 = {{"nh1", 4444}}; EXPECT_CALL(*config, findBrokerAddressOverride(b1.broker_id_)).WillOnce(Return(cr1)); - absl::optional cr2 = absl::nullopt; + std::optional cr2 = std::nullopt; EXPECT_CALL(*config, findBrokerAddressOverride(b2.broker_id_)).WillOnce(Return(cr2)); - absl::optional cr3 = {{"nh3", 6666}}; + std::optional cr3 = {{"nh3", 6666}}; EXPECT_CALL(*config, findBrokerAddressOverride(b3.broker_id_)).WillOnce(Return(cr3)); ResponseRewriterImpl testee{config}; diff --git a/contrib/kafka/filters/network/test/kafka_request_parser_test.cc b/contrib/kafka/filters/network/test/kafka_request_parser_test.cc index c2a0436cadf3d..fceca9698a07d 100644 --- a/contrib/kafka/filters/network/test/kafka_request_parser_test.cc +++ b/contrib/kafka/filters/network/test/kafka_request_parser_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/kafka/filters/network/source/kafka_request_parser.h" #include "contrib/kafka/filters/network/test/buffer_based_test.h" #include "contrib/kafka/filters/network/test/serialization_utilities.h" @@ -109,7 +111,7 @@ TEST_F(KafkaRequestParserTest, RequestDataParserShouldHandleDeserializerExceptio int32_t get() const override { throw std::runtime_error("should not be invoked at all"); }; }; - RequestContextSharedPtr request_context{new RequestContext{1024, {0, 0, 0, absl::nullopt}}}; + RequestContextSharedPtr request_context{new RequestContext{1024, {0, 0, 0, std::nullopt}}}; RequestDataParser testee{request_context}; absl::string_view data = putGarbageIntoBuffer(); @@ -144,7 +146,7 @@ TEST_F(KafkaRequestParserTest, // given const int32_t request_size = 1024; // There are still 1024 bytes to read to complete the request. RequestContextSharedPtr request_context{ - new RequestContext{request_size, {0, 0, 0, absl::nullopt}}}; + new RequestContext{request_size, {0, 0, 0, std::nullopt}}}; RequestDataParser testee{request_context}; diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/api_versions_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/api_versions_unit_test.cc index 4d9cb58a6995e..6a916397b383a 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/api_versions_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/api_versions_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/kafka/filters/network/source/mesh/command_handlers/api_versions.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -20,7 +22,7 @@ TEST(ApiVersionsTest, shouldBeAlwaysReadyForAnswer) { // given MockAbstractRequestListener filter; EXPECT_CALL(filter, onRequestReadyForAnswer()); - const RequestHeader header = {API_VERSIONS_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {API_VERSIONS_REQUEST_API_KEY, 0, 0, std::nullopt}; ApiVersionsRequestHolder testee = {filter, header}; // when, then - invoking should immediately notify the filter. diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/fetch_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/fetch_unit_test.cc index 2b65bce9c7527..9ddb27f0a2fd7 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/fetch_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/fetch_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/mocks/event/mocks.h" #include "contrib/kafka/filters/network/source/mesh/command_handlers/fetch.h" @@ -46,7 +48,7 @@ class FetchUnitTest : public testing::Test { FetchUnitTest() { ON_CALL(filter_, dispatcher).WillByDefault(ReturnRef(dispatcher_)); } std::shared_ptr makeTestee() { - const RequestHeader header = {FETCH_REQUEST_API_KEY, 0, TEST_CORRELATION_ID, absl::nullopt}; + const RequestHeader header = {FETCH_REQUEST_API_KEY, 0, TEST_CORRELATION_ID, std::nullopt}; // Our request refers to aaa-0, aaa-1, bbb-10, bbb-20. const FetchTopic t1 = {"aaa", {{0, 0, 0}, {1, 0, 0}}}; const FetchTopic t2 = {"bbb", {{10, 0, 0}, {20, 0, 0}}}; @@ -98,7 +100,7 @@ TEST_F(FetchUnitTest, ShouldCleanupAfterTimer) { // Helper method to generate records. InboundRecordSharedPtr makeRecord() { - return std::make_shared("aaa", 0, 0, absl::nullopt, absl::nullopt); + return std::make_shared("aaa", 0, 0, std::nullopt, std::nullopt); } TEST_F(FetchUnitTest, ShouldReceiveRecords) { diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/list_offsets_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/list_offsets_unit_test.cc index 4e38208763371..407005210c08e 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/list_offsets_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/list_offsets_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/kafka/filters/network/source/mesh/command_handlers/list_offsets.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -20,7 +22,7 @@ TEST(ListOffsetsTest, shouldBeAlwaysReadyForAnswer) { // given MockAbstractRequestListener filter; EXPECT_CALL(filter, onRequestReadyForAnswer()); - const RequestHeader header = {LIST_OFFSETS_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {LIST_OFFSETS_REQUEST_API_KEY, 0, 0, std::nullopt}; const ListOffsetsRequest data = {0, {}}; const auto message = std::make_shared>(header, data); ListOffsetsRequestHolder testee = {filter, message}; diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/metadata_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/metadata_unit_test.cc index e40aa2923e224..0f3264403cf1b 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/metadata_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/metadata_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/kafka/filters/network/source/external/responses.h" #include "contrib/kafka/filters/network/source/mesh/command_handlers/metadata.h" #include "gmock/gmock.h" @@ -21,7 +23,7 @@ class MockAbstractRequestListener : public AbstractRequestListener { class MockUpstreamKafkaConfiguration : public UpstreamKafkaConfiguration { public: - MOCK_METHOD(absl::optional, computeClusterConfigForTopic, (const std::string&), + MOCK_METHOD(std::optional, computeClusterConfigForTopic, (const std::string&), (const)); MOCK_METHOD((std::pair), getAdvertisedAddress, (), (const)); }; @@ -36,16 +38,15 @@ TEST(MetadataTest, shouldBeAlwaysReadyForAnswer) { // First topic is going to have configuration present (42 partitions for each topic). const ClusterConfig topic1config = {"", 42, {}, {}}; EXPECT_CALL(configuration, computeClusterConfigForTopic("topic1")) - .WillOnce(Return(absl::make_optional(topic1config))); + .WillOnce(Return(std::make_optional(topic1config))); // Second topic is not going to have configuration present. - EXPECT_CALL(configuration, computeClusterConfigForTopic("topic2")) - .WillOnce(Return(absl::nullopt)); + EXPECT_CALL(configuration, computeClusterConfigForTopic("topic2")).WillOnce(Return(std::nullopt)); const RequestHeader header = {METADATA_REQUEST_API_KEY, METADATA_REQUEST_MAX_VERSION, 0, - absl::nullopt}; + std::nullopt}; const MetadataRequestTopic t1 = MetadataRequestTopic{"topic1"}; const MetadataRequestTopic t2 = MetadataRequestTopic{"topic2"}; // Third topic is not going to have an explicit name. - const MetadataRequestTopic t3 = MetadataRequestTopic{Uuid{13, 42}, absl::nullopt, TaggedFields{}}; + const MetadataRequestTopic t3 = MetadataRequestTopic{Uuid{13, 42}, std::nullopt, TaggedFields{}}; const MetadataRequest data = {{t1, t2, t3}}; const auto message = std::make_shared>(header, data); MetadataRequestHolder testee = {filter, configuration, message}; diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/produce_record_extractor_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/produce_record_extractor_unit_test.cc index 068aa40c1334b..64743dec112e5 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/produce_record_extractor_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/produce_record_extractor_unit_test.cc @@ -1,3 +1,4 @@ +#include #include #include "test/test_common/utility.h" @@ -95,7 +96,7 @@ TEST(RecordExtractorImpl, shouldProcessRecordBytes) { const TopicProduceData tpd1 = {"topic1", {t1_ppd1, t1_ppd2, t1_ppd3}}; // Weird input from client, protocol allows sending null value as bytes array. - const PartitionProduceData t2_ppd = {20, absl::nullopt}; + const PartitionProduceData t2_ppd = {20, std::nullopt}; const TopicProduceData tpd2 = {"topic2", {t2_ppd}}; const std::vector input = {tpd1, tpd2}; diff --git a/contrib/kafka/filters/network/test/mesh/command_handlers/produce_unit_test.cc b/contrib/kafka/filters/network/test/mesh/command_handlers/produce_unit_test.cc index f98fec3fb5e16..8797abbf6934a 100644 --- a/contrib/kafka/filters/network/test/mesh/command_handlers/produce_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/command_handlers/produce_unit_test.cc @@ -1,3 +1,4 @@ +#include #include #include "test/test_common/utility.h" @@ -57,7 +58,7 @@ TEST_F(ProduceUnitTest, ShouldHandleProduceRequestWithNoRecords) { const std::vector records = {}; EXPECT_CALL(extractor_, extractRecords(_)).WillOnce(Return(records)); - const RequestHeader header = {0, 0, 0, absl::nullopt}; + const RequestHeader header = {0, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); ProduceRequestHolder testee = {filter_, upstream_kafka_facade_, extractor_, message}; @@ -86,7 +87,7 @@ TEST_F(ProduceUnitTest, ShouldSendRecordsInNormalFlow) { const std::vector records = {r1, r2}; EXPECT_CALL(extractor_, extractRecords(_)).WillOnce(Return(records)); - const RequestHeader header = {0, 0, 0, absl::nullopt}; + const RequestHeader header = {0, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); std::shared_ptr testee = @@ -144,7 +145,7 @@ TEST_F(ProduceUnitTest, ShouldMergeOutboundRecordResponses) { const std::vector records = {r1, r2}; EXPECT_CALL(extractor_, extractRecords(_)).WillOnce(Return(records)); - const RequestHeader header = {0, 0, 0, absl::nullopt}; + const RequestHeader header = {0, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); std::shared_ptr testee = @@ -198,7 +199,7 @@ TEST_F(ProduceUnitTest, ShouldHandleDeliveryErrors) { const std::vector records = {r1, r2}; EXPECT_CALL(extractor_, extractRecords(_)).WillOnce(Return(records)); - const RequestHeader header = {0, 0, 0, absl::nullopt}; + const RequestHeader header = {0, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); std::shared_ptr testee = @@ -248,7 +249,7 @@ TEST_F(ProduceUnitTest, ShouldIgnoreMementoFromAnotherRequest) { const std::vector records = {r1}; EXPECT_CALL(extractor_, extractRecords(_)).WillOnce(Return(records)); - const RequestHeader header = {0, 0, 0, absl::nullopt}; + const RequestHeader header = {0, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); std::shared_ptr testee = diff --git a/contrib/kafka/filters/network/test/mesh/filter_unit_test.cc b/contrib/kafka/filters/network/test/mesh/filter_unit_test.cc index fe73ab1cc9f3e..57edc079dc901 100644 --- a/contrib/kafka/filters/network/test/mesh/filter_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/filter_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/mocks/network/mocks.h" #include "contrib/kafka/filters/network/source/mesh/filter.h" @@ -179,7 +181,7 @@ class MockUpstreamKafkaConfiguration : public UpstreamKafkaConfiguration { public: MOCK_METHOD(void, onData, (Buffer::Instance&)); MOCK_METHOD(void, reset, ()); - MOCK_METHOD(absl::optional, computeClusterConfigForTopic, + MOCK_METHOD(std::optional, computeClusterConfigForTopic, (const std::string& topic), (const)); MOCK_METHOD((std::pair), getAdvertisedAddress, (), (const)); }; diff --git a/contrib/kafka/filters/network/test/mesh/request_processor_unit_test.cc b/contrib/kafka/filters/network/test/mesh/request_processor_unit_test.cc index 7f624eda4ad1a..3b85e6d0ce844 100644 --- a/contrib/kafka/filters/network/test/mesh/request_processor_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/request_processor_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/mocks/event/mocks.h" #include "test/test_common/utility.h" @@ -50,7 +52,7 @@ class MockRecordCallbackProcessor : public RecordCallbackProcessor { class MockUpstreamKafkaConfiguration : public UpstreamKafkaConfiguration { public: - MOCK_METHOD(absl::optional, computeClusterConfigForTopic, (const std::string&), + MOCK_METHOD(std::optional, computeClusterConfigForTopic, (const std::string&), (const)); MOCK_METHOD((std::pair), getAdvertisedAddress, (), (const)); }; @@ -67,7 +69,7 @@ class RequestProcessorTest : public testing::Test { TEST_F(RequestProcessorTest, ShouldProcessProduceRequest) { // given - const RequestHeader header = {PRODUCE_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {PRODUCE_REQUEST_API_KEY, 0, 0, std::nullopt}; const ProduceRequest data = {0, 0, {}}; const auto message = std::make_shared>(header, data); @@ -83,7 +85,7 @@ TEST_F(RequestProcessorTest, ShouldProcessProduceRequest) { TEST_F(RequestProcessorTest, ShouldProcessFetchRequest) { // given - const RequestHeader header = {FETCH_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {FETCH_REQUEST_API_KEY, 0, 0, std::nullopt}; const FetchRequest data = {0, 0, 0, {}}; const auto message = std::make_shared>(header, data); @@ -99,7 +101,7 @@ TEST_F(RequestProcessorTest, ShouldProcessFetchRequest) { TEST_F(RequestProcessorTest, ShouldProcessListOffsetsRequest) { // given - const RequestHeader header = {LIST_OFFSETS_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {LIST_OFFSETS_REQUEST_API_KEY, 0, 0, std::nullopt}; const ListOffsetsRequest data = {0, {}}; const auto message = std::make_shared>(header, data); @@ -115,8 +117,8 @@ TEST_F(RequestProcessorTest, ShouldProcessListOffsetsRequest) { TEST_F(RequestProcessorTest, ShouldProcessMetadataRequest) { // given - const RequestHeader header = {METADATA_REQUEST_API_KEY, 0, 0, absl::nullopt}; - const MetadataRequest data = {absl::nullopt}; + const RequestHeader header = {METADATA_REQUEST_API_KEY, 0, 0, std::nullopt}; + const MetadataRequest data = {std::nullopt}; const auto message = std::make_shared>(header, data); InFlightRequestSharedPtr capture = nullptr; @@ -131,7 +133,7 @@ TEST_F(RequestProcessorTest, ShouldProcessMetadataRequest) { TEST_F(RequestProcessorTest, ShouldProcessApiVersionsRequest) { // given - const RequestHeader header = {API_VERSIONS_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {API_VERSIONS_REQUEST_API_KEY, 0, 0, std::nullopt}; const ApiVersionsRequest data = {}; const auto message = std::make_shared>(header, data); @@ -147,7 +149,7 @@ TEST_F(RequestProcessorTest, ShouldProcessApiVersionsRequest) { TEST_F(RequestProcessorTest, ShouldHandleUnsupportedRequest) { // given - const RequestHeader header = {END_TXN_REQUEST_API_KEY, 0, 0, absl::nullopt}; + const RequestHeader header = {END_TXN_REQUEST_API_KEY, 0, 0, std::nullopt}; const EndTxnRequest data = {"", 0, 0, false}; const auto message = std::make_shared>(header, data); @@ -157,7 +159,7 @@ TEST_F(RequestProcessorTest, ShouldHandleUnsupportedRequest) { TEST_F(RequestProcessorTest, ShouldHandleUnparseableRequest) { // given - const RequestHeader header = {42, 42, 42, absl::nullopt}; + const RequestHeader header = {42, 42, 42, std::nullopt}; const auto arg = std::make_shared(header); // when, then - exception gets thrown. diff --git a/contrib/kafka/filters/network/test/mesh/shared_consumer_manager_impl_unit_test.cc b/contrib/kafka/filters/network/test/mesh/shared_consumer_manager_impl_unit_test.cc index cd892a7526ea9..b15054190b168 100644 --- a/contrib/kafka/filters/network/test/mesh/shared_consumer_manager_impl_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/shared_consumer_manager_impl_unit_test.cc @@ -1,4 +1,5 @@ #include +#include #include "envoy/common/exception.h" #include "envoy/thread/thread.h" @@ -22,7 +23,7 @@ using testing::Return; class MockUpstreamKafkaConfiguration : public UpstreamKafkaConfiguration { public: - MOCK_METHOD(absl::optional, computeClusterConfigForTopic, (const std::string&), + MOCK_METHOD(std::optional, computeClusterConfigForTopic, (const std::string&), (const)); MOCK_METHOD((std::pair), getAdvertisedAddress, (), (const)); }; @@ -80,7 +81,7 @@ TEST_F(SharedConsumerManagerTest, ShouldHandleMissingConfig) { // given const std::string topic = "topic"; - EXPECT_CALL(configuration_, computeClusterConfigForTopic(topic)).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(configuration_, computeClusterConfigForTopic(topic)).WillOnce(Return(std::nullopt)); EXPECT_CALL(consumer_factory_, createConsumer(_, _, _, _, _)).Times(0); @@ -144,7 +145,7 @@ class RecordDistributorTest : public testing::Test { } InboundRecordSharedPtr makeRecord(const std::string& topic, const int32_t partition) { - return std::make_shared(topic, partition, 0, absl::nullopt, absl::nullopt); + return std::make_shared(topic, partition, 0, std::nullopt, std::nullopt); } }; diff --git a/contrib/kafka/filters/network/test/mesh/upstream_kafka_client_impl_unit_test.cc b/contrib/kafka/filters/network/test/mesh/upstream_kafka_client_impl_unit_test.cc index 10478d4892ebd..923d2a36c8d06 100644 --- a/contrib/kafka/filters/network/test/mesh/upstream_kafka_client_impl_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/upstream_kafka_client_impl_unit_test.cc @@ -30,7 +30,7 @@ class UpstreamKafkaClientTest : public testing::Test { protected: Event::MockDispatcher dispatcher_; Thread::ThreadFactory& thread_factory_ = Thread::threadFactoryForTest(); - NiceMock kafka_utils_{}; + NiceMock kafka_utils_; RawKafkaConfig config_ = {{"key1", "value1"}, {"key2", "value2"}}; std::unique_ptr producer_ptr_ = std::make_unique(); diff --git a/contrib/kafka/filters/network/test/mesh/upstream_kafka_facade_unit_test.cc b/contrib/kafka/filters/network/test/mesh/upstream_kafka_facade_unit_test.cc index ee66b7c172b9d..8f282657d83d1 100644 --- a/contrib/kafka/filters/network/test/mesh/upstream_kafka_facade_unit_test.cc +++ b/contrib/kafka/filters/network/test/mesh/upstream_kafka_facade_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "envoy/thread/thread.h" #include "envoy/thread_local/thread_local.h" @@ -20,7 +22,7 @@ namespace { class MockUpstreamKafkaConfiguration : public UpstreamKafkaConfiguration { public: - MOCK_METHOD(absl::optional, computeClusterConfigForTopic, (const std::string&), + MOCK_METHOD(std::optional, computeClusterConfigForTopic, (const std::string&), (const)); MOCK_METHOD((std::pair), getAdvertisedAddress, (), (const)); }; @@ -87,7 +89,7 @@ TEST(UpstreamKafkaFacadeTest, shouldThrowIfThereIsNoConfigurationForGivenTopic) MockUpstreamKafkaConfiguration configuration; const ClusterConfig cluster_config = { "cluster", 1, {{"bootstrap.servers", "localhost:9092"}}, {}}; - EXPECT_CALL(configuration, computeClusterConfigForTopic(topic)).WillOnce(Return(absl::nullopt)); + EXPECT_CALL(configuration, computeClusterConfigForTopic(topic)).WillOnce(Return(std::nullopt)); ThreadLocal::MockInstance slot_allocator; EXPECT_CALL(slot_allocator, allocateSlot()) .WillOnce(Invoke(&slot_allocator, &ThreadLocal::MockInstance::allocateSlotMock)); diff --git a/contrib/kafka/filters/network/test/protocol/requests_test_cc.j2 b/contrib/kafka/filters/network/test/protocol/requests_test_cc.j2 index 9ce37e7cc6029..70ade8c778318 100644 --- a/contrib/kafka/filters/network/test/protocol/requests_test_cc.j2 +++ b/contrib/kafka/filters/network/test/protocol/requests_test_cc.j2 @@ -58,7 +58,7 @@ TEST_F(RequestTest, ShouldParse{{ message_type.name }}V{{ field_list.version }}) // given {{ message_type.name }} data = { {{ field_list.example_value() }} }; Request<{{ message_type.name }}> message = { { - {{ message_type.get_extra('api_key') }}, {{ field_list.version }}, 0, absl::nullopt }, data }; + {{ message_type.get_extra('api_key') }}, {{ field_list.version }}, 0, std::nullopt }, data }; // when auto received = serializeAndDeserialize(message); diff --git a/contrib/kafka/filters/network/test/request_codec_unit_test.cc b/contrib/kafka/filters/network/test/request_codec_unit_test.cc index 9a49b45d64316..53d4481973a25 100644 --- a/contrib/kafka/filters/network/test/request_codec_unit_test.cc +++ b/contrib/kafka/filters/network/test/request_codec_unit_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/kafka/filters/network/source/request_codec.h" #include "contrib/kafka/filters/network/test/buffer_based_test.h" #include "gmock/gmock.h" @@ -44,7 +46,7 @@ using MockRequestCallbackSharedPtr = std::shared_ptr; class RequestCodecUnitTest : public testing::Test, public BufferBasedTest { protected: MockParserFactory initial_parser_factory_{}; - MockRequestParserResolver parser_resolver_{}; + MockRequestParserResolver parser_resolver_; MockRequestCallbackSharedPtr callback_{std::make_shared()}; }; @@ -136,7 +138,7 @@ TEST_F(RequestCodecUnitTest, ShouldPassParsedMessageToCallbackAndInitializeNextP putGarbageIntoBuffer(); const AbstractRequestSharedPtr parsed_message = - std::make_shared>(RequestHeader{0, 0, 0, absl::nullopt}, 0); + std::make_shared>(RequestHeader{0, 0, 0, std::nullopt}, 0); MockParserSharedPtr parser1 = std::make_shared(); EXPECT_CALL(*parser1, parse(_)) @@ -167,7 +169,7 @@ TEST_F(RequestCodecUnitTest, ShouldPassParseFailureDataToCallback) { putGarbageIntoBuffer(); const RequestParseFailureSharedPtr failure_data = - std::make_shared(RequestHeader{0, 0, 0, absl::nullopt}); + std::make_shared(RequestHeader{0, 0, 0, std::nullopt}); MockParserSharedPtr parser = std::make_shared(); auto consume_and_return = [&failure_data](absl::string_view& data) -> RequestParseResponse { diff --git a/contrib/kafka/filters/network/test/response_codec_unit_test.cc b/contrib/kafka/filters/network/test/response_codec_unit_test.cc index ce208b1985005..6cb268bcfcf42 100644 --- a/contrib/kafka/filters/network/test/response_codec_unit_test.cc +++ b/contrib/kafka/filters/network/test/response_codec_unit_test.cc @@ -44,7 +44,7 @@ using MockResponseCallbackSharedPtr = std::shared_ptr; class ResponseCodecUnitTest : public testing::Test, public BufferBasedTest { protected: MockResponseInitialParserFactory factory_{}; - MockResponseParserResolver parser_resolver_{}; + MockResponseParserResolver parser_resolver_; MockResponseCallbackSharedPtr callback_{std::make_shared()}; }; diff --git a/contrib/kafka/filters/network/test/serialization_test.cc b/contrib/kafka/filters/network/test/serialization_test.cc index bbb80c3bd8f74..0b4c17f892ce8 100644 --- a/contrib/kafka/filters/network/test/serialization_test.cc +++ b/contrib/kafka/filters/network/test/serialization_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/test_common/utility.h" #include "contrib/kafka/filters/network/source/tagged_fields.h" @@ -306,7 +308,7 @@ TEST(NullableStringDeserializer, ShouldDeserializeEmptyString) { TEST(NullableStringDeserializer, ShouldDeserializeAbsentString) { // given - const NullableString value = absl::nullopt; + const NullableString value = std::nullopt; serializeThenDeserializeAndCheckEquality(value); } @@ -341,7 +343,7 @@ TEST(NullableCompactStringDeserializer, ShouldDeserializeEmptyString) { TEST(NullableCompactStringDeserializer, ShouldDeserializeAbsentString) { // given - const NullableString value = absl::nullopt; + const NullableString value = std::nullopt; serializeCompactThenDeserializeAndCheckEquality(value); } @@ -413,7 +415,7 @@ TEST(NullableBytesDeserializer, ShouldDeserializeEmptyBytes) { } TEST(NullableBytesDeserializer, ShouldDeserializeNullBytes) { - const NullableBytes value = absl::nullopt; + const NullableBytes value = std::nullopt; serializeThenDeserializeAndCheckEquality(value); } @@ -445,7 +447,7 @@ TEST(NullableCompactBytesDeserializer, ShouldDeserializeEmptyBytes) { } TEST(NullableCompactBytesDeserializer, ShouldDeserializeNullBytes) { - const NullableBytes value = absl::nullopt; + const NullableBytes value = std::nullopt; serializeCompactThenDeserializeAndCheckEquality(value); } @@ -502,7 +504,7 @@ TEST(NullableArrayDeserializer, ShouldConsumeCorrectAmountOfData) { } TEST(NullableArrayDeserializer, ShouldConsumeNullArray) { - const NullableArray value = absl::nullopt; + const NullableArray value = std::nullopt; serializeThenDeserializeAndCheckEquality>(value); } @@ -530,7 +532,7 @@ TEST(NullableCompactArrayDeserializer, ShouldConsumeCorrectAmountOfData) { } TEST(NullableCompactArrayDeserializer, ShouldConsumeNullArray) { - const NullableArray value = absl::nullopt; + const NullableArray value = std::nullopt; serializeCompactThenDeserializeAndCheckEquality< NullableCompactArrayDeserializer>(value); } @@ -556,7 +558,7 @@ TEST(NullableStructDeserializer, ShouldConsumeCorrectAmountOfData) { } TEST(NullableStructDeserializer, ShouldConsumeNullStruct) { - const ExampleNSD::ResponseType value = absl::nullopt; + const ExampleNSD::ResponseType value = std::nullopt; serializeThenDeserializeAndCheckEquality(value); } @@ -574,9 +576,52 @@ TEST(TaggedFieldDeserializer, ShouldConsumeCorrectAmountOfData) { serializeCompactThenDeserializeAndCheckEquality(value); } +TEST(TaggedFieldDeserializer, ShouldConsumeEmptyData) { + const TaggedField value{0, Bytes{}}; + serializeCompactThenDeserializeAndCheckEquality(value); +} + +TEST(TaggedFieldDeserializer, ShouldConsumeDataInChunks) { + const TaggedField value{42, Bytes{10, 20, 30, 40, 50}}; + serializeCompactThenDeserializeAndCheckEqualityWithChunks(value); +} + +TEST(TaggedFieldDeserializer, ShouldThrowOnExcessiveDataLength) { + // given + TaggedFieldDeserializer testee; + Buffer::OwnedImpl buffer; + + const uint32_t tag = 0; + encoder.encodeCompact(tag, buffer); + const uint32_t oversized_length = TaggedFieldDeserializer::MAX_TAGGED_FIELD_DATA_SIZE + 1; + encoder.encodeCompact(oversized_length, buffer); + + absl::string_view data = {getRawData(buffer), buffer.length()}; + + // when, then + EXPECT_THROW_WITH_REGEX(testee.feed(data), EnvoyException, "exceeds maximum allowed"); +} + +TEST(TaggedFieldDeserializer, ShouldAcceptDataLengthAtLimit) { + // given + TaggedFieldDeserializer testee; + Buffer::OwnedImpl buffer; + + const uint32_t tag = 0; + encoder.encodeCompact(tag, buffer); + const uint32_t max_length = TaggedFieldDeserializer::MAX_TAGGED_FIELD_DATA_SIZE; + encoder.encodeCompact(max_length, buffer); + + absl::string_view data = {getRawData(buffer), buffer.length()}; + + // when, then + EXPECT_NO_THROW(testee.feed(data)); + ASSERT_EQ(testee.ready(), false); +} + TEST(TaggedFieldsDeserializer, ShouldConsumeCorrectAmountOfData) { std::vector fields; - for (uint32_t i = 0; i < 200; ++i) { + for (uint32_t i = 0; i < 10; ++i) { const TaggedField tagged_field = {i, Bytes{1, 2, 3, 4}}; fields.push_back(tagged_field); } @@ -584,6 +629,40 @@ TEST(TaggedFieldsDeserializer, ShouldConsumeCorrectAmountOfData) { serializeCompactThenDeserializeAndCheckEquality(value); } +TEST(TaggedFieldsDeserializer, ShouldConsumeZeroFields) { + const TaggedFields value{{}}; + serializeCompactThenDeserializeAndCheckEquality(value); +} + +TEST(TaggedFieldsDeserializer, ShouldThrowOnExcessiveFieldCount) { + // given + TaggedFieldsDeserializer testee; + Buffer::OwnedImpl buffer; + + const uint32_t oversized_count = TaggedFieldsDeserializer::MAX_TAGGED_FIELD_COUNT + 1; + encoder.encodeCompact(oversized_count, buffer); + + absl::string_view data = {getRawData(buffer), buffer.length()}; + + // when, then + EXPECT_THROW_WITH_REGEX(testee.feed(data), EnvoyException, "exceeds maximum allowed"); +} + +TEST(TaggedFieldsDeserializer, ShouldAcceptFieldCountAtLimit) { + // given + TaggedFieldsDeserializer testee; + Buffer::OwnedImpl buffer; + + const uint32_t max_count = TaggedFieldsDeserializer::MAX_TAGGED_FIELD_COUNT; + encoder.encodeCompact(max_count, buffer); + + absl::string_view data = {getRawData(buffer), buffer.length()}; + + // when, then + EXPECT_NO_THROW(testee.feed(data)); + ASSERT_EQ(testee.ready(), false); +} + // Just a helper to write shorter tests. template Bytes toBytes(uint32_t fn(const T arg, Bytes& out), const T arg) { Bytes res; diff --git a/contrib/mysql_proxy/filters/network/source/BUILD b/contrib/mysql_proxy/filters/network/source/BUILD index 5bb9bf73dc62a..90a93dd087164 100644 --- a/contrib/mysql_proxy/filters/network/source/BUILD +++ b/contrib/mysql_proxy/filters/network/source/BUILD @@ -27,8 +27,10 @@ envoy_cc_library( "//envoy/server:filter_config_interface", "//envoy/stats:stats_interface", "//envoy/stats:stats_macros", + "//source/common/crypto:utility_lib", "//source/common/network:filter_lib", "//source/extensions/filters/network:well_known_names", + "@envoy_api//contrib/envoy/extensions/filters/network/mysql_proxy/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -78,6 +80,7 @@ envoy_cc_library( name = "util_lib", srcs = ["mysql_utils.cc"], hdrs = ["mysql_utils.h"], + external_deps = ["ssl"], deps = [ ":codec_interface", "//source/common/buffer:buffer_lib", diff --git a/contrib/mysql_proxy/filters/network/source/mysql_codec.h b/contrib/mysql_proxy/filters/network/source/mysql_codec.h index d040ab3fb6dbd..af6b3cc425455 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_codec.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_codec.h @@ -33,6 +33,10 @@ constexpr uint8_t MYSQL_RESP_MORE = 0x01; constexpr uint8_t MYSQL_RESP_AUTH_SWITCH = 0xfe; constexpr uint8_t MYSQL_RESP_ERR = 0xff; +constexpr uint8_t MYSQL_CACHINGSHA2_FAST_AUTH_SUCCESS = 0x03; +constexpr uint8_t MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED = 0x04; +constexpr uint8_t MYSQL_REQUEST_PUBLIC_KEY = 0x02; + constexpr uint8_t EOF_MARKER = 0xfe; constexpr uint8_t ERR_MARKER = 0xff; @@ -102,6 +106,7 @@ constexpr uint8_t LENENCODINT_3BYTES = 0xfd; constexpr uint8_t LENENCODINT_8BYTES = 0xfe; constexpr uint32_t DEFAULT_MAX_PACKET_SIZE = (1 << 24) - 1; // 16M-1 +constexpr uint32_t SSL_CONNECTION_REQUEST_PACKET_SIZE = 32; constexpr uint8_t MIN_PROTOCOL_VERSION = 10; constexpr char MYSQL_STR_END = '\0'; diff --git a/contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.cc b/contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.cc index a2b6e3664cb07..602575b6e9cf5 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.cc @@ -71,7 +71,7 @@ DecodeStatus ClientLogin::parseMessage(Buffer::Instance& buffer, uint32_t len) { return DecodeStatus::Failure; } setBaseClientCap(base_cap); - if (base_cap & CLIENT_SSL) { + if (len == SSL_CONNECTION_REQUEST_PACKET_SIZE && (base_cap & CLIENT_SSL)) { return parseResponseSsl(buffer); } if (base_cap & CLIENT_PROTOCOL_41) { diff --git a/contrib/mysql_proxy/filters/network/source/mysql_config.cc b/contrib/mysql_proxy/filters/network/source/mysql_config.cc index c5e3b32d66ec1..1292a14a81ce7 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_config.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_config.cc @@ -28,8 +28,8 @@ NetworkFilters::MySQLProxy::MySQLConfigFactory::createFilterFactoryFromProtoType const std::string stat_prefix = fmt::format("mysql.{}", proto_config.stat_prefix()); - MySQLFilterConfigSharedPtr filter_config( - std::make_shared(stat_prefix, context.scope())); + MySQLFilterConfigSharedPtr filter_config(std::make_shared( + stat_prefix, context.scope(), proto_config.downstream_ssl())); return [filter_config](Network::FilterManager& filter_manager) -> void { filter_manager.addFilter(std::make_shared(filter_config)); }; diff --git a/contrib/mysql_proxy/filters/network/source/mysql_decoder.h b/contrib/mysql_proxy/filters/network/source/mysql_decoder.h index 491cb93a49d26..63f406d42b8e5 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_decoder.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_decoder.h @@ -7,6 +7,7 @@ #include "contrib/mysql_proxy/filters/network/source/mysql_codec_greeting.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec_switch_resp.h" #include "contrib/mysql_proxy/filters/network/source/mysql_session.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_utils.h" namespace Envoy { namespace Extensions { @@ -21,14 +22,15 @@ class DecoderCallbacks { virtual ~DecoderCallbacks() = default; virtual void onProtocolError() PURE; - virtual void onNewMessage(MySQLSession::State) PURE; virtual void onServerGreeting(ServerGreeting&) PURE; - virtual void onClientLogin(ClientLogin&) PURE; + virtual void onClientLogin(ClientLogin&, MySQLSession::State) PURE; virtual void onClientLoginResponse(ClientLoginResponse&) PURE; virtual void onClientSwitchResponse(ClientSwitchResponse&) PURE; virtual void onMoreClientLoginResponse(ClientLoginResponse&) PURE; virtual void onCommand(Command&) PURE; virtual void onCommandResponse(CommandResponse&) PURE; + virtual void onAuthSwitchMoreClientData(std::unique_ptr data) PURE; + virtual bool onSSLRequest() PURE; }; /** @@ -38,16 +40,33 @@ class Decoder { public: virtual ~Decoder() = default; - virtual void onData(Buffer::Instance& data) PURE; + enum class Result { + ReadyForNext, // Decoder processed previous message and is ready for the next message. + Stopped // Received and processed message disrupts the current flow. Decoder stopped accepting + // data. This happens when decoder wants filter to perform some action, for example to + // call starttls transport socket to enable TLS. + }; + + struct PayloadMetadata { + uint8_t seq; + uint32_t len; + }; + + virtual Result onData(Buffer::Instance& data, bool is_upstream) PURE; virtual MySQLSession& getSession() PURE; const Extensions::Common::SQLUtils::SQLUtils::DecoderAttributes& getAttributes() const { return attributes_; } + const std::vector& getPayloadMetadataList() const { + return payload_metadata_list_; + } + protected: // Decoder attributes. Extensions::Common::SQLUtils::SQLUtils::DecoderAttributes attributes_; + std::vector payload_metadata_list_; }; using DecoderPtr = std::unique_ptr; diff --git a/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.cc b/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.cc index 36d28b7688093..47f233a6cd524 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.cc @@ -11,8 +11,10 @@ namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t len) { - ENVOY_LOG(trace, "mysql_proxy: parsing message, seq {}, len {}", seq, len); +void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t len, + bool is_upstream) { + ENVOY_LOG(trace, "mysql_proxy: parsing message, seq {}, len {}, is_upstream {}", seq, len, + is_upstream); // Run the MySQL state machine switch (session_.getState()) { case MySQLSession::State::Init: { @@ -27,14 +29,15 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t // Process Client Handshake Response ClientLogin client_login{}; client_login.decode(message, seq, len); - if (client_login.isSSLRequest()) { + + if (len == SSL_CONNECTION_REQUEST_PACKET_SIZE && client_login.isSSLRequest()) { session_.setState(MySQLSession::State::SslPt); } else if (client_login.isResponse41()) { session_.setState(MySQLSession::State::ChallengeResp41); } else { session_.setState(MySQLSession::State::ChallengeResp320); } - callbacks_.onClientLogin(client_login); + callbacks_.onClientLogin(client_login, session_.getState()); break; } case MySQLSession::State::SslPt: @@ -48,6 +51,7 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t session_.setState(MySQLSession::State::NotHandled); break; } + ENVOY_LOG(trace, "mysql_proxy: ChallengeResp resp_code is {}", resp_code); std::unique_ptr msg; MySQLSession::State state = MySQLSession::State::NotHandled; switch (resp_code) { @@ -55,7 +59,7 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t msg = std::make_unique(); state = MySQLSession::State::Req; // reset seq# when entering the REQ state - session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); + session_.resetSeq(); break; } case MYSQL_RESP_AUTH_SWITCH: { @@ -70,6 +74,7 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t } case MYSQL_RESP_MORE: { msg = std::make_unique(); + state = MySQLSession::State::AuthSwitchMore; break; } default: @@ -92,7 +97,12 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t case MySQLSession::State::AuthSwitchMore: { uint8_t resp_code; - if (BufferHelper::peekUint8(message, resp_code) != DecodeStatus::Success) { + if (is_upstream) { + std::unique_ptr secure_data; + BufferHelper::readSecureBytes(message, len, secure_data); + callbacks_.onAuthSwitchMoreClientData(std::move(secure_data)); + break; + } else if (BufferHelper::peekUint8(message, resp_code) != DecodeStatus::Success) { session_.setState(MySQLSession::State::NotHandled); break; } @@ -102,19 +112,19 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t case MYSQL_RESP_OK: { msg = std::make_unique(); state = MySQLSession::State::Req; - session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); + session_.resetSeq(); break; } case MYSQL_RESP_MORE: { msg = std::make_unique(); - state = MySQLSession::State::AuthSwitchResp; + state = MySQLSession::State::AuthSwitchMore; break; } case MYSQL_RESP_ERR: { msg = std::make_unique(); // stop parsing auth req/response, attempt to resync in command state state = MySQLSession::State::Resync; - session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); + session_.resetSeq(); break; } case MYSQL_RESP_AUTH_SWITCH: { @@ -165,59 +175,83 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t static_cast(session_.getState())); } -bool DecoderImpl::decode(Buffer::Instance& data) { +bool DecoderImpl::decode(Buffer::Instance& data, bool is_upstream) { ENVOY_LOG(trace, "mysql_proxy: decoding {} bytes", data.length()); uint32_t len = 0; uint8_t seq = 0; + bool return_without_parse = false; + bool result = true; + + const auto current_state = session_.getState(); // ignore ssl message - if (session_.getState() == MySQLSession::State::SslPt) { + if (current_state == MySQLSession::State::SslPt) { data.drain(data.length()); - return true; + return result; } if (BufferHelper::peekHdr(data, len, seq) != DecodeStatus::Success) { throw EnvoyException("error parsing mysql packet header"); } ENVOY_LOG(trace, "mysql_proxy: seq {}, len {}", seq, len); + // If message is split over multiple packets, hold off until the entire message is available. // Consider the size of the header here as it's not consumed yet. if (sizeof(uint32_t) + len > data.length()) { - return false; + return_without_parse = true; + result = false; } - BufferHelper::consumeHdr(data); // Consume the header once the message is fully available. - callbacks_.onNewMessage(session_.getState()); - // Ignore duplicate and out-of-sync packets. - if (seq != session_.getExpectedSeq()) { + if (seq != session_.getExpectedSeq(is_upstream)) { // case when server response is over, and client send req if (session_.getState() == MySQLSession::State::ReqResp && seq == MYSQL_REQUEST_PKT_NUM) { - session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); + session_.resetSeq(); session_.setState(MySQLSession::State::Req); } else { ENVOY_LOG(info, "mysql_proxy: ignoring out-of-sync packet"); callbacks_.onProtocolError(); - data.drain(len); // Ensure that the whole message was consumed - return true; + data.drain(sizeof(uint32_t) + len); // Ensure that the whole message was consumed + return_without_parse = true; } } - session_.setExpectedSeq(seq + 1); + + payload_metadata_list_.push_back( + {.seq = session_.convertToSeqOnReciever(seq, is_upstream), .len = len}); + + if (return_without_parse) { + return result; + } + + BufferHelper::consumeHdr(data); // Consume the header once the message is fully available. + session_.incSeq(); const ssize_t data_len = data.length(); - parseMessage(data, seq, len); + parseMessage(data, seq, len, is_upstream); const ssize_t consumed_len = data_len - data.length(); data.drain(len - consumed_len); // Ensure that the whole message was consumed ENVOY_LOG(trace, "mysql_proxy: {} bytes remaining in buffer", data.length()); - return true; + return result; } -void DecoderImpl::onData(Buffer::Instance& data) { +Decoder::Result DecoderImpl::onData(Buffer::Instance& data, bool is_upstream) { + payload_metadata_list_.clear(); + // TODO(venilnoronha): handle messages over 16 mb. See // https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_packets.html#sect_protocol_basic_packets_sending_mt_16mb. - while (!BufferHelper::endOfBuffer(data) && decode(data)) { + while (!BufferHelper::endOfBuffer(data) && decode(data, is_upstream)) { } + + if (is_upstream && session_.getState() == MySQLSession::State::SslPt) { + if (!callbacks_.onSSLRequest()) { + session_.adjustSeqOffset(1); + session_.setState(MySQLSession::State::ChallengeReq); + return Decoder::Result::Stopped; + } + } + + return Decoder::Result::ReadyForNext; } DecoderFactoryImpl DecoderFactoryImpl::instance_; diff --git a/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.h b/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.h index 6c126edaeba1f..85c8678e64e71 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.h @@ -11,12 +11,12 @@ class DecoderImpl : public Decoder, public Logger::Loggable DecoderImpl(DecoderCallbacks& callbacks) : callbacks_(callbacks) {} // MySQLProxy::Decoder - void onData(Buffer::Instance& data) override; + Decoder::Result onData(Buffer::Instance& data, bool is_upstream) override; MySQLSession& getSession() override { return session_; } private: - bool decode(Buffer::Instance& data); - void parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t len); + bool decode(Buffer::Instance& data, bool is_upstream); + void parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t len, bool is_upstream); DecoderCallbacks& callbacks_; MySQLSession session_; diff --git a/contrib/mysql_proxy/filters/network/source/mysql_filter.cc b/contrib/mysql_proxy/filters/network/source/mysql_filter.cc index 1574bd2a23e80..d09b830913c8b 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_filter.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_filter.cc @@ -1,23 +1,30 @@ #include "contrib/mysql_proxy/filters/network/source/mysql_filter.h" +#include + #include "envoy/config/core/v3/base.pb.h" #include "source/common/buffer/buffer_impl.h" #include "source/common/common/assert.h" #include "source/common/common/logger.h" +#include "source/common/crypto/utility.h" #include "source/extensions/filters/network/well_known_names.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin_resp.h" #include "contrib/mysql_proxy/filters/network/source/mysql_decoder_impl.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_utils.h" +#include "openssl/evp.h" +#include "openssl/rsa.h" namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -MySQLFilterConfig::MySQLFilterConfig(const std::string& stat_prefix, Stats::Scope& scope) - : scope_(scope), stats_(generateStats(stat_prefix, scope)) {} +MySQLFilterConfig::MySQLFilterConfig(const std::string& stat_prefix, Stats::Scope& scope, + SSLMode downstream_ssl) + : scope_(scope), stats_(generateStats(stat_prefix, scope)), downstream_ssl_(downstream_ssl) {} MySQLFilter::MySQLFilter(MySQLFilterConfigSharedPtr config) : config_(std::move(config)) {} @@ -25,27 +32,145 @@ void MySQLFilter::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& ca read_callbacks_ = &callbacks; } +void MySQLFilter::initializeWriteFilterCallbacks(Network::WriteFilterCallbacks& callbacks) { + write_callbacks_ = &callbacks; +} + Network::FilterStatus MySQLFilter::onData(Buffer::Instance& data, bool) { + Network::FilterStatus status = Network::FilterStatus::Continue; + uint64_t remaining = read_buffer_.length(); + // Safety measure just to make sure that if we have a decoding error we keep going and lose stats. // This can be removed once we are more confident of this code. - if (sniffing_) { - read_buffer_.add(data); - doDecode(read_buffer_); + if (!sniffing_) { + return status; } - return Network::FilterStatus::Continue; + + read_buffer_.add(data); + status = doDecode(read_buffer_, true); + + if (status == Network::FilterStatus::StopIteration) { + data.drain(data.length()); + return status; + } + + // RSA mediation: intercept client cleartext password. + if (rsa_auth_state_ == RsaAuthState::WaitingClientPassword && cleartext_password_) { + data.drain(data.length()); + + uint8_t inject_seq = getSession().getExpectedSeq(false) - 1; + ENVOY_CONN_LOG(trace, + "mysql_proxy: intercepted client password, sending request-public-key (seq={})", + read_callbacks_->connection(), inject_seq); + + Buffer::OwnedImpl buf; + BufferHelper::addUint24(buf, 1); + BufferHelper::addUint8(buf, inject_seq); + BufferHelper::addUint8(buf, MYSQL_REQUEST_PUBLIC_KEY); + read_callbacks_->injectReadDataToFilterChain(buf, false); + + rsa_auth_state_ = RsaAuthState::WaitingServerKey; + return Network::FilterStatus::StopIteration; + } + + if (config_->terminateSsl()) { + doRewrite(data, remaining, true); + } + + return status; } Network::FilterStatus MySQLFilter::onWrite(Buffer::Instance& data, bool) { + Network::FilterStatus status = Network::FilterStatus::Continue; + // Safety measure just to make sure that if we have a decoding error we keep going and lose stats. // This can be removed once we are more confident of this code. - if (sniffing_) { + if (!sniffing_) { + return status; + } + + // RSA mediation: intercept server's public key response. + if (rsa_auth_state_ == RsaAuthState::WaitingServerKey) { write_buffer_.add(data); - doDecode(write_buffer_); + data.drain(data.length()); + + // Check if we have a complete packet. + uint32_t len = 0; + uint8_t seq = 0; + if (BufferHelper::peekHdr(write_buffer_, len, seq) != DecodeStatus::Success || + sizeof(uint32_t) + len > write_buffer_.length()) { + ENVOY_CONN_LOG(trace, + "mysql_proxy: waiting for complete public key packet ({} bytes buffered)", + read_callbacks_->connection(), write_buffer_.length()); + return Network::FilterStatus::StopIteration; + } + + ENVOY_CONN_LOG(trace, "mysql_proxy: received server public key packet (seq={}, len={})", + read_callbacks_->connection(), seq, len); + + // Full packet available. Parse it: [hdr][0x01 marker][PEM key bytes]. + BufferHelper::consumeHdr(write_buffer_); + uint8_t marker; + BufferHelper::readUint8(write_buffer_, marker); + if (marker != MYSQL_RESP_MORE) { + ENVOY_CONN_LOG(error, "mysql_proxy: unexpected marker 0x{:02x} in public key response", + read_callbacks_->connection(), marker); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return Network::FilterStatus::StopIteration; + } + + std::string pem_key; + BufferHelper::readStringBySize(write_buffer_, len - 1, pem_key); + write_buffer_.drain(write_buffer_.length()); + + ENVOY_CONN_LOG(trace, "mysql_proxy: extracted PEM key ({} bytes), encrypting password", + read_callbacks_->connection(), pem_key.size()); + + sendEncryptedPassword(pem_key, seq); + getSession().adjustSeqOffset(-2); + rsa_auth_state_ = RsaAuthState::WaitingServerResult; + + ENVOY_CONN_LOG(trace, "mysql_proxy: RSA encrypted password sent, waiting for server result", + read_callbacks_->connection()); + + // Nothing to forward to client; data was already drained. + return Network::FilterStatus::Continue; } - return Network::FilterStatus::Continue; + + uint64_t remaining = write_buffer_.length(); + + write_buffer_.add(data); + status = doDecode(write_buffer_, false); + + if (status == Network::FilterStatus::StopIteration) { + data.drain(data.length()); + return status; + } + + if (config_->terminateSsl()) { + doRewrite(data, remaining, false); + } + + return status; +} + +bool MySQLFilter::onSSLRequest() { + if (!config_->terminateSsl()) { + return true; + } + + if (!read_callbacks_->connection().startSecureTransport()) { + ENVOY_CONN_LOG(info, "mysql_proxy: cannot enable secure transport. Check configuration.", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + } else { + ENVOY_CONN_LOG(trace, "mysql_proxy: enabled SSL termination.", read_callbacks_->connection()); + } + + return false; } -void MySQLFilter::doDecode(Buffer::Instance& buffer) { +Network::FilterStatus MySQLFilter::doDecode(Buffer::Instance& buffer, bool is_upstream) { // Clear dynamic metadata. envoy::config::core::v3::Metadata& dynamic_metadata = read_callbacks_->connection().streamInfo().dynamicMetadata(); @@ -58,7 +183,12 @@ void MySQLFilter::doDecode(Buffer::Instance& buffer) { } try { - decoder_->onData(buffer); + switch (decoder_->onData(buffer, is_upstream)) { + case Decoder::Result::ReadyForNext: + return Network::FilterStatus::Continue; + case Decoder::Result::Stopped: + return Network::FilterStatus::StopIteration; + } } catch (EnvoyException& e) { ENVOY_LOG(info, "mysql_proxy: decoding error: {}", e.what()); config_->stats_.decoder_errors_.inc(); @@ -66,38 +196,230 @@ void MySQLFilter::doDecode(Buffer::Instance& buffer) { read_buffer_.drain(read_buffer_.length()); write_buffer_.drain(write_buffer_.length()); } + + return Network::FilterStatus::Continue; } DecoderPtr MySQLFilter::createDecoder(DecoderCallbacks& callbacks) { return std::make_unique(callbacks); } +void MySQLFilter::rewritePacketHeader(Buffer::Instance& data, uint8_t seq, uint32_t len) { + BufferHelper::consumeHdr(data); + BufferHelper::addUint24(data, len); + BufferHelper::addUint8(data, seq); +} + +void MySQLFilter::stripSslCapability(Buffer::Instance& data) { + uint32_t client_cap = 0; + BufferHelper::readUint32(data, client_cap); + BufferHelper::addUint32(data, client_cap & ~CLIENT_SSL); +} + +void MySQLFilter::doRewrite(Buffer::Instance& data, uint64_t remaining, bool is_upstream) { + MySQLSession::State state = getSession().getState(); + auto& payload_metadata_list = decoder_->getPayloadMetadataList(); + const uint64_t original_data_size = data.length(); + uint64_t max_data_size = original_data_size; + + for (size_t i = 0; i < payload_metadata_list.size(); ++i) { + uint8_t seq = payload_metadata_list[i].seq; + uint32_t len = payload_metadata_list[i].len; + + if (i == 0 && remaining > 0) { + // First packet spans old internal buffer and new data. The header and first + // (remaining - 4) payload bytes are in the internal buffer, not in data. + ASSERT(remaining >= 4, "partial header should not appear in payload metadata"); + len -= remaining - 4; + } else { + rewritePacketHeader(data, seq, len); + max_data_size -= 4; + + if (is_upstream && (state == MySQLSession::State::ChallengeResp41 || + state == MySQLSession::State::ChallengeResp320)) { + stripSslCapability(data); + len -= 4; + } + } + + uint64_t copy_size = std::min(static_cast(len), max_data_size); + std::string payload; + payload.reserve(copy_size); + BufferHelper::readStringBySize(data, copy_size, payload); + BufferHelper::addBytes(data, payload.c_str(), payload.size()); + max_data_size -= copy_size; + } + + ASSERT(data.length() == original_data_size, "doRewrite must not change overall buffer size"); +} + void MySQLFilter::onProtocolError() { config_->stats_.protocol_errors_.inc(); } -void MySQLFilter::onNewMessage(MySQLSession::State state) { - if (state == MySQLSession::State::ChallengeReq) { - config_->stats_.login_attempts_.inc(); +void MySQLFilter::onServerGreeting(ServerGreeting& greeting) { + config_->stats_.login_attempts_.inc(); + ENVOY_CONN_LOG(trace, "mysql_proxy: server greeting: version={}, auth_plugin={}, scramble_len={}", + read_callbacks_->connection(), greeting.getVersion(), greeting.getAuthPluginName(), + greeting.getAuthPluginData().size()); + if (config_->terminateSsl()) { + server_scramble_ = greeting.getAuthPluginData(); + // The MySQL greeting protocol may include a trailing null filler byte in + // auth_plugin_data, making it 21 bytes. The actual nonce used by + // caching_sha2_password is always 20 bytes. Truncate to avoid corrupting + // the XOR for passwords longer than 20 characters. + if (server_scramble_.size() > NATIVE_PSSWORD_HASH_LENGTH) { + server_scramble_.resize(NATIVE_PSSWORD_HASH_LENGTH); + } + auth_plugin_name_ = greeting.getAuthPluginName(); + ENVOY_CONN_LOG(trace, + "mysql_proxy: captured scramble ({} bytes) for SSL termination, plugin={}", + read_callbacks_->connection(), server_scramble_.size(), auth_plugin_name_); } } -void MySQLFilter::onClientLogin(ClientLogin& client_login) { +void MySQLFilter::onClientLogin(ClientLogin& client_login, MySQLSession::State state) { + ENVOY_CONN_LOG(trace, "mysql_proxy: client login: ssl_request={}, state={}, user={}", + read_callbacks_->connection(), client_login.isSSLRequest(), + static_cast(state), client_login.getUsername()); if (client_login.isSSLRequest()) { config_->stats_.upgraded_to_ssl_.inc(); } + + if (state == MySQLSession::State::ChallengeResp41 || + state == MySQLSession::State::ChallengeResp320) { + // REQUIRE mode: reject clients that did not initiate SSL. + using MySQLProto = envoy::extensions::filters::network::mysql_proxy::v3::MySQLProxy; + if (config_->downstream_ssl_ == MySQLProto::REQUIRE && getSession().getSeqOffset() == 0) { + ENVOY_CONN_LOG(info, + "mysql_proxy: downstream_ssl=REQUIRE but client did not initiate SSL, closing", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + } + } } void MySQLFilter::onClientLoginResponse(ClientLoginResponse& client_login_resp) { + ENVOY_CONN_LOG(trace, "mysql_proxy: server login response: resp_code=0x{:02x}", + read_callbacks_->connection(), client_login_resp.getRespCode()); if (client_login_resp.getRespCode() == MYSQL_RESP_AUTH_SWITCH) { config_->stats_.auth_switch_request_.inc(); } else if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { config_->stats_.login_failures_.inc(); + } else if (config_->terminateSsl() && getSession().getSeqOffset() != 0 && + client_login_resp.getRespCode() == MYSQL_RESP_MORE) { + auto* auth_more = dynamic_cast(&client_login_resp); + if (auth_more && !auth_more->getAuthMoreData().empty()) { + ENVOY_CONN_LOG(trace, "mysql_proxy: AuthMoreData[0]=0x{:02x}, plugin={}", + read_callbacks_->connection(), auth_more->getAuthMoreData()[0], + auth_plugin_name_); + if (auth_more->getAuthMoreData()[0] == MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED && + auth_plugin_name_ == "caching_sha2_password") { + rsa_auth_state_ = RsaAuthState::WaitingClientPassword; + ENVOY_CONN_LOG(trace, "mysql_proxy: full auth required, entering RSA mediation", + read_callbacks_->connection()); + } else if (auth_more->getAuthMoreData()[0] == MYSQL_CACHINGSHA2_FAST_AUTH_SUCCESS) { + ENVOY_CONN_LOG(trace, "mysql_proxy: fast auth success (cache hit), no RSA needed", + read_callbacks_->connection()); + } + } } } void MySQLFilter::onMoreClientLoginResponse(ClientLoginResponse& client_login_resp) { + ENVOY_CONN_LOG(trace, "mysql_proxy: more login response: resp_code=0x{:02x}, rsa_state={}", + read_callbacks_->connection(), client_login_resp.getRespCode(), + static_cast(rsa_auth_state_)); if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { config_->stats_.login_failures_.inc(); } + if (rsa_auth_state_ == RsaAuthState::WaitingServerResult) { + ENVOY_CONN_LOG(trace, "mysql_proxy: RSA mediation complete, result=0x{:02x}", + read_callbacks_->connection(), client_login_resp.getRespCode()); + rsa_auth_state_ = RsaAuthState::Inactive; + } +} + +void MySQLFilter::onAuthSwitchMoreClientData(std::unique_ptr data) { + ENVOY_CONN_LOG(trace, "mysql_proxy: client auth data received, len={}, rsa_state={}", + read_callbacks_->connection(), data ? data->size() : 0, + static_cast(rsa_auth_state_)); + if (rsa_auth_state_ == RsaAuthState::WaitingClientPassword && data) { + // Password arrives in SecureBytes (guarded memory, zeroed on free). + // The decoder already read it via readSecureBytes which zeroed the source buffer. + cleartext_password_ = std::move(data); + ENVOY_CONN_LOG(trace, "mysql_proxy: captured cleartext password ({} bytes) in secure memory", + read_callbacks_->connection(), cleartext_password_->size()); + } +} + +void MySQLFilter::sendEncryptedPassword(const std::string& pem_key, uint8_t last_server_seq) { + if (!cleartext_password_) { + ENVOY_CONN_LOG(error, "mysql_proxy: no cleartext password captured", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return; + } + + // XOR password with scramble in secure memory. The cleartext_password_ includes the + // trailing null sent by the client (password\0 format for caching_sha2_password). + SecureBytes xored(cleartext_password_->size()); + for (size_t i = 0; i < cleartext_password_->size(); i++) { + xored[i] = (*cleartext_password_)[i] ^ server_scramble_[i % server_scramble_.size()]; + } + + // Import the server's public key. + auto pkey = Envoy::Common::Crypto::UtilitySingleton::get().importPublicKeyPEM(pem_key); + if (!pkey || !pkey->getEVP_PKEY()) { + ENVOY_CONN_LOG(error, "mysql_proxy: failed to import server public key", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return; + } + + // RSA-encrypt with OAEP/SHA-1 padding (MySQL requirement). + bssl::UniquePtr ctx(EVP_PKEY_CTX_new(pkey->getEVP_PKEY(), nullptr)); + if (!ctx || EVP_PKEY_encrypt_init(ctx.get()) <= 0 || + EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0 || + EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), EVP_sha1()) <= 0) { + ENVOY_CONN_LOG(error, "mysql_proxy: failed to initialize RSA encryption context", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return; + } + + size_t out_len = 0; + if (EVP_PKEY_encrypt(ctx.get(), nullptr, &out_len, xored.data(), xored.size()) <= 0) { + ENVOY_CONN_LOG(error, "mysql_proxy: failed to determine RSA ciphertext length", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + cleartext_password_.reset(); + return; + } + + std::vector encrypted(out_len); + if (EVP_PKEY_encrypt(ctx.get(), encrypted.data(), &out_len, xored.data(), xored.size()) <= 0) { + ENVOY_CONN_LOG(error, "mysql_proxy: RSA encryption failed", read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + cleartext_password_.reset(); + return; + } + encrypted.resize(out_len); + + // Build the encrypted password packet: [3-byte len][seq][encrypted_data]. + uint8_t enc_seq = static_cast(last_server_seq + 1); + Buffer::OwnedImpl buf; + BufferHelper::addUint24(buf, encrypted.size()); + BufferHelper::addUint8(buf, enc_seq); + BufferHelper::addVector(buf, encrypted); + + ENVOY_CONN_LOG(trace, + "mysql_proxy: injecting RSA-encrypted password (seq={}, {} bytes ciphertext)", + read_callbacks_->connection(), enc_seq, encrypted.size()); + + // Inject toward server (bypasses our filter). + read_callbacks_->injectReadDataToFilterChain(buf, false); + + // Securely destroy the cleartext password (OPENSSL_cleanse zeroes before freeing). + cleartext_password_.reset(); } void MySQLFilter::onCommand(Command& command) { diff --git a/contrib/mysql_proxy/filters/network/source/mysql_filter.h b/contrib/mysql_proxy/filters/network/source/mysql_filter.h index bf41c53762269..33928f9181ee9 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_filter.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_filter.h @@ -9,6 +9,7 @@ #include "source/common/common/logger.h" +#include "contrib/envoy/extensions/filters/network/mysql_proxy/v3/mysql_proxy.pb.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin_resp.h" @@ -17,6 +18,7 @@ #include "contrib/mysql_proxy/filters/network/source/mysql_codec_switch_resp.h" #include "contrib/mysql_proxy/filters/network/source/mysql_decoder.h" #include "contrib/mysql_proxy/filters/network/source/mysql_session.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_utils.h" namespace Envoy { namespace Extensions { @@ -49,12 +51,19 @@ struct MySQLProxyStats { */ class MySQLFilterConfig { public: - MySQLFilterConfig(const std::string& stat_prefix, Stats::Scope& scope); + using SSLMode = envoy::extensions::filters::network::mysql_proxy::v3::MySQLProxy::SSLMode; + + MySQLFilterConfig(const std::string& stat_prefix, Stats::Scope& scope, SSLMode downstream_ssl); const MySQLProxyStats& stats() { return stats_; } + bool terminateSsl() const { + return downstream_ssl_ != + envoy::extensions::filters::network::mysql_proxy::v3::MySQLProxy::DISABLE; + } Stats::Scope& scope_; MySQLProxyStats stats_; + SSLMode downstream_ssl_; private: MySQLProxyStats generateStats(const std::string& prefix, Stats::Scope& scope) { @@ -64,6 +73,13 @@ class MySQLFilterConfig { using MySQLFilterConfigSharedPtr = std::shared_ptr; +enum class RsaAuthState { + Inactive, // Normal operation + WaitingClientPassword, // Server sent AuthMoreData(0x04), forwarded to client, waiting for pw + WaitingServerKey, // Sent 0x02 to server, waiting for PEM public key + WaitingServerResult, // Sent RSA-encrypted pw, waiting for OK/ERR +}; + /** * Implementation of MySQL proxy filter. */ @@ -79,29 +95,47 @@ class MySQLFilter : public Network::Filter, DecoderCallbacks, Logger::Loggable data) override; + bool onSSLRequest() override; - void doDecode(Buffer::Instance& buffer); + Network::FilterStatus doDecode(Buffer::Instance& buffer, bool is_upstream); DecoderPtr createDecoder(DecoderCallbacks& callbacks); + void doRewrite(Buffer::Instance& buffer, uint64_t remaining, bool is_upstream); MySQLSession& getSession() { return decoder_->getSession(); } + // Helpers for doRewrite. + static void rewritePacketHeader(Buffer::Instance& data, uint8_t seq, uint32_t len); + static void stripSslCapability(Buffer::Instance& data); + + RsaAuthState getRsaAuthState() const { return rsa_auth_state_; } + private: + void sendEncryptedPassword(const std::string& pem_key, uint8_t last_server_seq); + Network::ReadFilterCallbacks* read_callbacks_{}; + Network::WriteFilterCallbacks* write_callbacks_{}; MySQLFilterConfigSharedPtr config_; Buffer::OwnedImpl read_buffer_; Buffer::OwnedImpl write_buffer_; std::unique_ptr decoder_; bool sniffing_{true}; + + // RSA mediation state for caching_sha2_password full authentication. + RsaAuthState rsa_auth_state_{RsaAuthState::Inactive}; + std::unique_ptr cleartext_password_; + std::vector server_scramble_; + std::string auth_plugin_name_; }; } // namespace MySQLProxy diff --git a/contrib/mysql_proxy/filters/network/source/mysql_session.h b/contrib/mysql_proxy/filters/network/source/mysql_session.h index 691d582633b11..c659c50b4554a 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_session.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_session.h @@ -27,12 +27,23 @@ class MySQLSession : Logger::Loggable { void setState(MySQLSession::State state) { state_ = state; } MySQLSession::State getState() { return state_; } - uint8_t getExpectedSeq() { return expected_seq_; } - void setExpectedSeq(uint8_t seq) { expected_seq_ = seq; } + uint8_t getExpectedSeq(bool is_upstream) { return seq_ - (is_upstream ? 0 : seq_offset_); } + uint8_t convertToSeqOnReciever(uint8_t seq, bool is_upstream) { + return seq - (is_upstream ? 1 : -1) * seq_offset_; + } + void resetSeq() { + seq_ = MYSQL_REQUEST_PKT_NUM; + seq_offset_ = 0; + } + void incSeq() { seq_++; } + int8_t getSeqOffset() const { return seq_offset_; } + void setSeqOffset(int8_t offset) { seq_offset_ = offset; } + void adjustSeqOffset(int8_t delta) { seq_offset_ += delta; } private: MySQLSession::State state_{State::Init}; - uint8_t expected_seq_{0}; + uint8_t seq_{0}; + int8_t seq_offset_{0}; }; } // namespace MySQLProxy diff --git a/contrib/mysql_proxy/filters/network/source/mysql_utils.cc b/contrib/mysql_proxy/filters/network/source/mysql_utils.cc index c51fffd2076ce..7c56ba6b0631e 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_utils.cc +++ b/contrib/mysql_proxy/filters/network/source/mysql_utils.cc @@ -216,6 +216,31 @@ DecodeStatus BufferHelper::peekHdr(Buffer::Instance& buffer, uint32_t& len, uint return DecodeStatus::Success; } +DecodeStatus BufferHelper::readSecureBytes(Buffer::Instance& buffer, size_t len, + std::unique_ptr& out) { + if (buffer.length() < len) { + return DecodeStatus::Failure; + } + + out = std::make_unique(len); + + // Copy data into secure memory, then zero the source buffer slices. + buffer.copyOut(0, len, out->data()); + + uint64_t zeroed = 0; + for (const auto& slice : buffer.getRawSlices()) { + if (zeroed >= len) { + break; + } + const uint64_t chunk = std::min(static_cast(slice.len_), len - zeroed); + OPENSSL_cleanse(slice.mem_, chunk); + zeroed += chunk; + } + + buffer.drain(len); + return DecodeStatus::Success; +} + } // namespace MySQLProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/contrib/mysql_proxy/filters/network/source/mysql_utils.h b/contrib/mysql_proxy/filters/network/source/mysql_utils.h index 254ce0f8edc81..d22bcdc8bab5b 100644 --- a/contrib/mysql_proxy/filters/network/source/mysql_utils.h +++ b/contrib/mysql_proxy/filters/network/source/mysql_utils.h @@ -8,12 +8,46 @@ #include "source/common/common/logger.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec.h" +#include "openssl/crypto.h" namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { +// Secure memory buffer that is guaranteed to be zeroed on destruction +// via OPENSSL_cleanse, preventing password leakage in memory. +class SecureBytes { +public: + explicit SecureBytes(size_t len) : data_(new uint8_t[len]), len_(len) {} + + ~SecureBytes() { + if (data_ != nullptr) { + OPENSSL_cleanse(data_, len_); + delete[] data_; + } + } + + SecureBytes(const SecureBytes&) = delete; + SecureBytes& operator=(const SecureBytes&) = delete; + + SecureBytes(SecureBytes&& other) noexcept : data_(other.data_), len_(other.len_) { + other.data_ = nullptr; + other.len_ = 0; + } + + uint8_t* data() { return data_; } + const uint8_t* data() const { return data_; } + size_t size() const { return len_; } + + uint8_t operator[](size_t i) const { return data_[i]; } + uint8_t& operator[](size_t i) { return data_[i]; } + +private: + uint8_t* data_{nullptr}; + size_t len_{0}; +}; + /** * IO helpers for reading/writing MySQL data from/to a buffer. * MySQL uses unsigned integer values in Little Endian format only. @@ -50,6 +84,11 @@ class BufferHelper : public Logger::Loggable { static DecodeStatus peekUint8(Buffer::Instance& buffer, uint8_t& val); static void consumeHdr(Buffer::Instance& buffer); static DecodeStatus peekHdr(Buffer::Instance& buffer, uint32_t& len, uint8_t& seq); + + // Read `len` bytes from buffer into a SecureBytes object backed by guarded memory, + // then zero the original data in the buffer to prevent password leakage. + static DecodeStatus readSecureBytes(Buffer::Instance& buffer, size_t len, + std::unique_ptr& out); }; } // namespace MySQLProxy diff --git a/contrib/mysql_proxy/filters/network/test/BUILD b/contrib/mysql_proxy/filters/network/test/BUILD index a29546b61dbd9..7a2d00086e919 100644 --- a/contrib/mysql_proxy/filters/network/test/BUILD +++ b/contrib/mysql_proxy/filters/network/test/BUILD @@ -78,9 +78,11 @@ envoy_cc_test( srcs = [ "mysql_filter_test.cc", ], + external_deps = ["ssl"], deps = [ ":mysql_test_utils_lib", "//contrib/mysql_proxy/filters/network/source:config", + "//source/common/crypto:utility_lib", "//test/mocks/network:network_mocks", ], ) @@ -104,6 +106,32 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "mysql_ssl_integration_test", + srcs = [ + "mysql_ssl_integration_test.cc", + ], + data = [ + "mysql_ssl_allow_test_config.yaml", + "mysql_ssl_disable_test_config.yaml", + "mysql_ssl_require_test_config.yaml", + "//test/config/integration/certs", + ], + external_deps = ["ssl"], + rbe_pool = "6gig", + deps = [ + ":mysql_test_utils_lib", + "//contrib/mysql_proxy/filters/network/source:config", + "//contrib/mysql_proxy/filters/network/source:filter_lib", + "//source/common/tcp_proxy", + "//source/extensions/filters/network/tcp_proxy:config", + "//source/extensions/transport_sockets/raw_buffer:config", + "//source/extensions/transport_sockets/starttls:config", + "//test/integration:integration_lib", + "@envoy_api//envoy/extensions/transport_sockets/raw_buffer/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "mysql_command_tests", srcs = [ diff --git a/contrib/mysql_proxy/filters/network/test/mysql_clogin_test.cc b/contrib/mysql_proxy/filters/network/test/mysql_clogin_test.cc index 3772e220eb265..1dd3d12df9eae 100644 --- a/contrib/mysql_proxy/filters/network/test/mysql_clogin_test.cc +++ b/contrib/mysql_proxy/filters/network/test/mysql_clogin_test.cc @@ -804,83 +804,6 @@ TEST_F(MySQLCLoginTest, MySQLClientLoginSSLEncDec) { EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); } -/* - * Negative Test the MYSQL Client login SSL message parser: - * Incomplete cap flag - */ -TEST_F(MySQLCLoginTest, MySQLClientLoginSslIncompleteCap) { - ClientLogin& mysql_clogin_encode = MySQLCLoginTest::getClientLogin(CLIENT_SSL); - Buffer::OwnedImpl buffer; - mysql_clogin_encode.encode(buffer); - - int incomplete_len = sizeof(mysql_clogin_encode.getClientCap()) - 1; - Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); - - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), 0); -} - -/* - * Negative Test the MYSQL Client login SSL message parser: - * Incomplete max packet - */ -TEST_F(MySQLCLoginTest, MySQLClientLoginSslIncompleteMaxPacket) { - ClientLogin& mysql_clogin_encode = MySQLCLoginTest::getClientLogin(CLIENT_SSL); - Buffer::OwnedImpl buffer; - mysql_clogin_encode.encode(buffer); - - int incomplete_len = sizeof(mysql_clogin_encode.getClientCap()); - Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); - - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); - EXPECT_TRUE(mysql_clogin_decode.isSSLRequest()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), 0); -} - -/* - * Negative Test the MYSQL Client login SSL message parser: - * Incomplete character set - */ -TEST_F(MySQLCLoginTest, MySQLClientLoginSslIncompleteCharset) { - ClientLogin& mysql_clogin_encode = MySQLCLoginTest::getClientLogin(CLIENT_SSL); - Buffer::OwnedImpl buffer; - mysql_clogin_encode.encode(buffer); - - int incomplete_len = - sizeof(mysql_clogin_encode.getClientCap()) + sizeof(mysql_clogin_encode.getMaxPacket()); - Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); - - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), 0); -} - -/* - * Negative Test the MYSQL Client login SSL message parser: - * Incomplete reserved - */ -TEST_F(MySQLCLoginTest, MySQLClientLoginSslIncompleteReserved) { - ClientLogin& mysql_clogin_encode = MySQLCLoginTest::getClientLogin(CLIENT_SSL); - Buffer::OwnedImpl buffer; - mysql_clogin_encode.encode(buffer); - - int incomplete_len = sizeof(mysql_clogin_encode.getClientCap()) + - sizeof(mysql_clogin_encode.getMaxPacket()) + - sizeof(mysql_clogin_encode.getCharset()); - Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); - - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); -} - } // namespace MySQLProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/contrib/mysql_proxy/filters/network/test/mysql_filter_test.cc b/contrib/mysql_proxy/filters/network/test/mysql_filter_test.cc index 1b816e86bd7b9..1a8d4d6178128 100644 --- a/contrib/mysql_proxy/filters/network/test/mysql_filter_test.cc +++ b/contrib/mysql_proxy/filters/network/test/mysql_filter_test.cc @@ -1,15 +1,23 @@ #include "source/common/buffer/buffer_impl.h" +#include "source/common/crypto/utility.h" #include "test/mocks/network/mocks.h" #include "contrib/mysql_proxy/filters/network/source/mysql_codec.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin_resp.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_greeting.h" #include "contrib/mysql_proxy/filters/network/source/mysql_filter.h" #include "contrib/mysql_proxy/filters/network/source/mysql_utils.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "mysql_test_utils.h" +#include "openssl/evp.h" +#include "openssl/pem.h" +#include "openssl/rsa.h" +using testing::_; using testing::NiceMock; +using testing::ReturnRef; namespace Envoy { namespace Extensions { @@ -22,10 +30,48 @@ class MySQLFilterTest : public testing::Test, public MySQLTestUtils { public: MySQLFilterTest() { ENVOY_LOG_MISC(info, "test"); } - void initialize() { - config_ = std::make_shared(stat_prefix_, scope_); + using MySQLProxyProto = envoy::extensions::filters::network::mysql_proxy::v3::MySQLProxy; + + void initialize(MySQLProxyProto::SSLMode downstream_ssl = MySQLProxyProto::DISABLE) { + config_ = std::make_shared(stat_prefix_, scope_, downstream_ssl); filter_ = std::make_unique(config_); filter_->initializeReadFilterCallbacks(filter_callbacks_); + filter_->initializeWriteFilterCallbacks(write_filter_callbacks_); + } + + // Encode a server greeting for caching_sha2_password with 20-byte scramble. + std::string encodeServerGreetingCachingSha2() { + ServerGreeting greeting; + greeting.setProtocol(MYSQL_PROTOCOL_10); + greeting.setVersion(getVersion()); + greeting.setThreadId(MYSQL_THREAD_ID); + greeting.setAuthPluginData(getAuthPluginData20()); + greeting.setServerCap(CLIENT_PLUGIN_AUTH | CLIENT_SECURE_CONNECTION); + greeting.setServerCharset(MYSQL_SERVER_LANGUAGE); + greeting.setServerStatus(MYSQL_SERVER_STATUS); + greeting.setAuthPluginName("caching_sha2_password"); + Buffer::OwnedImpl buffer; + greeting.encode(buffer); + BufferHelper::encodeHdr(buffer, GREETING_SEQ_NUM); + return buffer.toString(); + } + + // Encode an AuthMoreData packet with specific data bytes. + std::string encodeAuthMoreDataPacket(const std::vector& data, uint8_t seq) { + AuthMoreMessage auth_more; + auth_more.setAuthMoreData(data); + Buffer::OwnedImpl buffer; + auth_more.encode(buffer); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); + } + + // Encode a raw client-to-server packet (e.g., cleartext password). + std::string encodeRawPacket(const std::string& payload, uint8_t seq) { + Buffer::OwnedImpl buffer; + BufferHelper::addString(buffer, payload); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); } MySQLFilterConfigSharedPtr config_; @@ -34,6 +80,8 @@ class MySQLFilterTest : public testing::Test, public MySQLTestUtils { Stats::Scope& scope_{*store_.rootScope()}; std::string stat_prefix_{"test."}; NiceMock filter_callbacks_; + NiceMock write_filter_callbacks_; + NiceMock connection_; }; // Test New Session counter increment @@ -54,11 +102,11 @@ TEST_F(MySQLFilterTest, MySqlFallbackToTcpProxy) { EXPECT_EQ(1UL, config_->stats().sessions_.value()); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(" ")); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(1UL, config_->stats().decoder_errors_.value()); Buffer::InstancePtr more_data(new Buffer::OwnedImpl("scooby doo - part 2!")); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*more_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*more_data, false)); } /** @@ -74,7 +122,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); @@ -85,7 +133,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); } @@ -210,7 +258,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41ErrTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); @@ -221,14 +269,14 @@ TEST_F(MySQLFilterTest, MySqlHandshake41ErrTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_ERR); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(1UL, config_->stats().login_failures_.value()); EXPECT_EQ(MySQLSession::State::Error, filter_->getSession().getState()); } /** * Test MySQL Handshake with protocol version 41 - * Server responds with Error + * Server responds with Auth More Data * SM: greeting(p=10) -> challenge-req(v41) -> serv-resp-more */ TEST_F(MySQLFilterTest, MySqlHandshake41AuthMoreTest) { @@ -240,7 +288,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41AuthMoreTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); @@ -251,8 +299,8 @@ TEST_F(MySQLFilterTest, MySqlHandshake41AuthMoreTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_MORE); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); - EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::AuthSwitchMore, filter_->getSession().getState()); } /** @@ -268,7 +316,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320OkTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -279,7 +327,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320OkTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); } @@ -296,7 +344,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320OkTestIncomplete) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -307,7 +355,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320OkTestIncomplete) { std::string srv_resp_data = encodeMessage(0); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -325,7 +373,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320ErrTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -336,7 +384,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320ErrTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_ERR); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(1UL, config_->stats().login_failures_.value()); EXPECT_EQ(MySQLSession::State::Error, filter_->getSession().getState()); } @@ -346,7 +394,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320ErrTest) { * State-machine moves to SSL-Pass-Through * SM: greeting(p=10) -> challenge-req(v320) -> SSL_PT */ -TEST_F(MySQLFilterTest, MySqlHandshakeSSLTest) { +TEST_F(MySQLFilterTest, MySqlHandshakeSSLPassThroughTest) { initialize(); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); @@ -355,23 +403,86 @@ TEST_F(MySQLFilterTest, MySqlHandshakeSSLTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(CLIENT_SSL | CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + // Send SSL Connection Request packet. + // https://dev.mysql.com/doc/internals/en/ssl-handshake.html + std::string clogin_data = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + // After SSL handshaking, attempt to login. + // Since the SSL-Pass-Through, # of login attempts is unknown. + clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + client_login_data = Buffer::InstancePtr(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); + EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + + std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, CHALLENGE_RESP_SEQ_NUM + 1); + Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + Buffer::OwnedImpl query_create_index("!@#$encr$#@!"); - BufferHelper::encodeHdr(query_create_index, 2); + BufferHelper::encodeHdr(query_create_index, 0); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_create_index, false)); EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); } +/** + * Test MySQL Handshake with SSL Request + * State-machine moves to SSL-Terminate + * SM: greeting(p=10) -> challenge-req(v320) -> SSL_PT -> ChallengeReq -> Req -> ReqResp + */ +TEST_F(MySQLFilterTest, MySqlHandshakeSSLTerminateTest) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + EXPECT_EQ(1UL, config_->stats().sessions_.value()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + + std::string clogin_data = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_CALL(connection_, close(_)).Times(0); + + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, + filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); + EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + + clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + client_login_data = Buffer::InstancePtr(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); + + std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + + Buffer::OwnedImpl query_create_index("!@#$encr$#@!"); + BufferHelper::encodeHdr(query_create_index, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_create_index, false)); + EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); +} + /** * Test MySQL Handshake with protocol version 320 * Server responds with Auth Switch @@ -387,7 +498,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -398,7 +509,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -407,7 +518,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchTest) { std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_OK, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); } @@ -426,7 +537,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchAuthSwitchTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -437,7 +548,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchAuthSwitchTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -446,7 +557,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchAuthSwitchTest) { std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -465,7 +576,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -476,7 +587,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -485,7 +596,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrTest) { std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_ERR, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::Resync, filter_->getSession().getState()); Command mysql_cmd_encode{}; @@ -516,7 +627,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchIncompleteRespcode) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -527,7 +638,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchIncompleteRespcode) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -536,7 +647,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchIncompleteRespcode) { std::string srv_resp_ok_data = encodeMessage(0, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -555,7 +666,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrFailResync) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -566,7 +677,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrFailResync) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -575,7 +686,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrFailResync) { std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_ERR, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::Resync, filter_->getSession().getState()); Command mysql_cmd_encode{}; @@ -590,7 +701,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrFailResync) { } /** - * Negative Testing MySQL Handshake with protocol version 320 + * MySQL Handshake with protocol version 320 * Server responds with Auth Switch More * SM: greeting(p=10) -> challenge-req(v320) -> serv-resp-auth-switch -> * -> auth_switch_resp -> serv-resp-auth-switch-more @@ -604,7 +715,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandMore) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -615,7 +726,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandMore) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -624,8 +735,8 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandMore) { std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_MORE, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); - EXPECT_EQ(MySQLSession::State::AuthSwitchResp, filter_->getSession().getState()); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); + EXPECT_EQ(MySQLSession::State::AuthSwitchMore, filter_->getSession().getState()); } /** @@ -643,7 +754,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandUnhandled) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -654,7 +765,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandUnhandled) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); std::string auth_switch_resp = encodeAuthSwitchResp(); Buffer::InstancePtr client_switch_resp(new Buffer::OwnedImpl(auth_switch_resp)); @@ -663,7 +774,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchMoreandUnhandled) { std::string srv_resp_ok_data = encodeClientLoginResp(0x32, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -681,24 +792,25 @@ TEST_F(MySQLFilterTest, MySqlHandshake41Ok2GreetTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string greeting_data2 = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data2(new Buffer::OwnedImpl(greeting_data2)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data2, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data2, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); EXPECT_EQ(1UL, config_->stats().protocol_errors_.value()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); - EXPECT_EQ(2UL, config_->stats().login_attempts_.value()); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); } @@ -717,7 +829,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41Ok2CloginTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); @@ -735,7 +847,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41Ok2CloginTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); } @@ -760,7 +872,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkOOOLoginTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); } @@ -786,12 +898,12 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkOOOFullLoginTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); EXPECT_EQ(2UL, config_->stats().protocol_errors_.value()); } @@ -811,12 +923,12 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkGreetingLoginOKTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); EXPECT_EQ(1UL, config_->stats_.protocol_errors_.value()); } @@ -835,7 +947,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320WrongCloginSeqTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", 2); @@ -860,7 +972,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchWrongSeqTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -876,12 +988,12 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchWrongSeqTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_AUTH_SWITCH); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::AuthSwitchResp, filter_->getSession().getState()); std::string srv_resp_ok_data = encodeClientLoginResp(MYSQL_RESP_OK, 1); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::AuthSwitchResp, filter_->getSession().getState()); } @@ -900,7 +1012,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320WrongServerRespCode) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -911,7 +1023,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake320WrongServerRespCode) { std::string srv_resp_ok_data = encodeClientLoginResp(0x53, 0); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); Buffer::OwnedImpl client_query_data; @@ -934,7 +1046,7 @@ TEST_F(MySQLFilterTest, MySqlWrongHdrPkt) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(0, "user1", CHALLENGE_SEQ_NUM); @@ -945,7 +1057,7 @@ TEST_F(MySQLFilterTest, MySqlWrongHdrPkt) { std::string srv_resp_ok_data = encodeClientLoginResp(0x53, 0); Buffer::InstancePtr server_resp_ok_data(new Buffer::OwnedImpl(srv_resp_ok_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); Buffer::OwnedImpl client_query_data("123"); @@ -968,7 +1080,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); @@ -979,7 +1091,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); Command mysql_cmd_encode{}; @@ -996,7 +1108,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 1); Buffer::InstancePtr request_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*request_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*request_resp_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); mysql_cmd_encode.setCmd(Command::Cmd::Query); @@ -1012,7 +1124,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 1); Buffer::InstancePtr show_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*show_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*show_resp_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); mysql_cmd_encode.setCmd(Command::Cmd::Query); @@ -1029,7 +1141,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 1); Buffer::InstancePtr create_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*create_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*create_resp_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); mysql_cmd_encode.setCmd(Command::Cmd::Query); @@ -1047,7 +1159,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 1); Buffer::InstancePtr create_index_resp_data(new Buffer::OwnedImpl(srv_resp_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, - filter_->onData(*create_index_resp_data, false)); + filter_->onWrite(*create_index_resp_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); mysql_cmd_encode.setCmd(Command::Cmd::FieldList); @@ -1064,10 +1176,802 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 1); Buffer::InstancePtr field_list_resp_data(new Buffer::OwnedImpl(srv_resp_data)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*field_list_resp_data, false)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*field_list_resp_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); } +/** + * Test RSA mediation for caching_sha2_password full authentication flow. + * SSL termination + cache miss: client sends cleartext password over TLS, + * filter RSA-encrypts it for the plaintext upstream connection. + */ +TEST_F(MySQLFilterTest, MySqlCachingSha2FullAuthRsaMediation) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Step 1: Server greeting with caching_sha2_password plugin. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + + // Step 2: Client SSL request (seq=1). + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + + // Step 3: Client login (seq=2 from client, should be rewritten to seq=1 for server). + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); + + // Step 4: Server responds with AuthMoreData(0x04) = full auth required (raw seq=2). + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(MySQLSession::State::AuthSwitchMore, filter_->getSession().getState()); + EXPECT_EQ(RsaAuthState::WaitingClientPassword, filter_->getRsaAuthState()); + + // Step 5: Client sends cleartext password (seq=4 from client perspective). + // The filter should intercept this and inject a request-public-key packet. + std::string password = "secret"; + std::string pw_payload = password + '\0'; + std::string pw_data = encodeRawPacket(pw_payload, 4); + + Buffer::OwnedImpl captured_request_key; + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)) + .WillOnce([&](Buffer::Instance& buf, bool) { captured_request_key.add(buf); }); + + Buffer::InstancePtr pw_buf(new Buffer::OwnedImpl(pw_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*pw_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerKey, filter_->getRsaAuthState()); + + // Verify the request-public-key packet: [len=1][seq=3][0x02] + ASSERT_EQ(5u, captured_request_key.length()); + uint32_t req_len = 0; + uint8_t req_seq = 0; + BufferHelper::peekHdr(captured_request_key, req_len, req_seq); + EXPECT_EQ(1u, req_len); + EXPECT_EQ(3u, req_seq); + BufferHelper::consumeHdr(captured_request_key); + uint8_t req_code; + BufferHelper::readUint8(captured_request_key, req_code); + EXPECT_EQ(MYSQL_REQUEST_PUBLIC_KEY, req_code); + + // Step 6: Generate an RSA-2048 key pair for the test. + bssl::UniquePtr gen_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + ASSERT_TRUE(gen_ctx); + ASSERT_GT(EVP_PKEY_keygen_init(gen_ctx.get()), 0); + ASSERT_GT(EVP_PKEY_CTX_set_rsa_keygen_bits(gen_ctx.get(), 2048), 0); + EVP_PKEY* raw_pkey = nullptr; + ASSERT_GT(EVP_PKEY_keygen(gen_ctx.get(), &raw_pkey), 0); + bssl::UniquePtr pkey(raw_pkey); + + // Extract public key PEM. + bssl::UniquePtr bio(BIO_new(BIO_s_mem())); + PEM_write_bio_PUBKEY(bio.get(), pkey.get()); + char* pem_data; + long pem_len = BIO_get_mem_data(bio.get(), &pem_data); + std::string pem_key(pem_data, pem_len); + + // Step 7: Server sends PEM key as AuthMoreData (raw seq=4). + // The filter should intercept this and inject the RSA-encrypted password. + Buffer::OwnedImpl captured_encrypted_pw; + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)) + .WillOnce([&](Buffer::Instance& buf, bool) { captured_encrypted_pw.add(buf); }); + + std::string key_packet = + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4); + Buffer::InstancePtr key_buf(new Buffer::OwnedImpl(key_packet)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*key_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerResult, filter_->getRsaAuthState()); + + // Verify the encrypted password packet header: [len=256][seq=5][encrypted_data] + ASSERT_GT(captured_encrypted_pw.length(), 4u); + uint32_t enc_len = 0; + uint8_t enc_seq = 0; + BufferHelper::peekHdr(captured_encrypted_pw, enc_len, enc_seq); + EXPECT_EQ(256u, enc_len); // RSA-2048 produces 256-byte ciphertext + EXPECT_EQ(5u, enc_seq); + + // Decrypt and verify the password XOR scramble. + BufferHelper::consumeHdr(captured_encrypted_pw); + std::string ciphertext; + BufferHelper::readStringBySize(captured_encrypted_pw, enc_len, ciphertext); + + bssl::UniquePtr dec_ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); + ASSERT_TRUE(dec_ctx); + ASSERT_GT(EVP_PKEY_decrypt_init(dec_ctx.get()), 0); + ASSERT_GT(EVP_PKEY_CTX_set_rsa_padding(dec_ctx.get(), RSA_PKCS1_OAEP_PADDING), 0); + ASSERT_GT(EVP_PKEY_CTX_set_rsa_oaep_md(dec_ctx.get(), EVP_sha1()), 0); + + size_t plain_len = 0; + ASSERT_GT(EVP_PKEY_decrypt(dec_ctx.get(), nullptr, &plain_len, + reinterpret_cast(ciphertext.data()), + ciphertext.size()), + 0); + std::vector plaintext(plain_len); + ASSERT_GT(EVP_PKEY_decrypt(dec_ctx.get(), plaintext.data(), &plain_len, + reinterpret_cast(ciphertext.data()), + ciphertext.size()), + 0); + plaintext.resize(plain_len); + + // plaintext = (password + \0) XOR scramble (cyclic 20-byte) + std::vector scramble = getAuthPluginData20(); // 20 bytes of 0xff + ASSERT_EQ(pw_payload.size(), plaintext.size()); + for (size_t i = 0; i < plaintext.size(); i++) { + uint8_t expected = static_cast(pw_payload[i]) ^ scramble[i % scramble.size()]; + EXPECT_EQ(expected, plaintext[i]) << "mismatch at byte " << i; + } + + // Step 8: Server sends OK (raw seq=6, rewritten to seq=5 for client). + std::string srv_ok = encodeClientLoginResp(MYSQL_RESP_OK, 0, 6); + Buffer::InstancePtr ok_buf(new Buffer::OwnedImpl(srv_ok)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*ok_buf, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * Test that fast auth success (AuthMoreData 0x03) passes through without RSA mediation. + */ +TEST_F(MySQLFilterTest, MySqlCachingSha2FastAuthPassthrough) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL request. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + + // Client login. + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); + + // Server responds with AuthMoreData(0x03) = fast auth success. + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FAST_AUTH_SUCCESS}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(MySQLSession::State::AuthSwitchMore, filter_->getSession().getState()); + // RSA mediation should NOT be triggered. + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); + + // Server sends OK (raw seq=3: next seq after server's AuthMoreData at seq=2). + std::string srv_ok = encodeClientLoginResp(MYSQL_RESP_OK, 0, 3); + Buffer::InstancePtr ok_buf(new Buffer::OwnedImpl(srv_ok)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*ok_buf, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); +} + +/** + * Test RSA mediation when server returns ERR after encrypted password. + */ +TEST_F(MySQLFilterTest, MySqlCachingSha2FullAuthRsaErr) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL request. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + + // Client login. + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + + // AuthMoreData(0x04) = full auth required. + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(RsaAuthState::WaitingClientPassword, filter_->getRsaAuthState()); + + // Client password. + std::string pw_data = encodeRawPacket(std::string("secret") + '\0', 4); + + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)); + Buffer::InstancePtr pw_buf(new Buffer::OwnedImpl(pw_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*pw_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerKey, filter_->getRsaAuthState()); + + // Generate RSA key pair and send PEM key. + bssl::UniquePtr gen_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + ASSERT_TRUE(gen_ctx); + ASSERT_GT(EVP_PKEY_keygen_init(gen_ctx.get()), 0); + ASSERT_GT(EVP_PKEY_CTX_set_rsa_keygen_bits(gen_ctx.get(), 2048), 0); + EVP_PKEY* raw_pkey = nullptr; + ASSERT_GT(EVP_PKEY_keygen(gen_ctx.get(), &raw_pkey), 0); + bssl::UniquePtr pkey(raw_pkey); + bssl::UniquePtr bio(BIO_new(BIO_s_mem())); + PEM_write_bio_PUBKEY(bio.get(), pkey.get()); + char* pem_data; + long pem_len = BIO_get_mem_data(bio.get(), &pem_data); + std::string pem_key(pem_data, pem_len); + + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)); + std::string key_packet = + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4); + Buffer::InstancePtr key_buf(new Buffer::OwnedImpl(key_packet)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*key_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerResult, filter_->getRsaAuthState()); + + // Server responds with ERR (raw seq=6). + std::string srv_err = encodeClientLoginResp(MYSQL_RESP_ERR, 0, 6); + Buffer::InstancePtr err_buf(new Buffer::OwnedImpl(srv_err)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*err_buf, false)); + EXPECT_EQ(1UL, config_->stats().login_failures_.value()); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * Test that RSA mediation is NOT triggered when downstream_ssl is DISABLE. + */ +TEST_F(MySQLFilterTest, MySqlCachingSha2NoTerminateSsl) { + initialize(); // downstream_ssl = DISABLE + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); + + // Client login (no SSL in this path). + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); + + // AuthMoreData(0x04) from server. + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + // RSA mediation should NOT be triggered because downstream_ssl is DISABLE. + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +// ============================================================================= +// Extended coverage: SSL Terminated, SSL Passthrough, No-SSL with queries +// ============================================================================= + +/** + * SSL Terminated: basic login with mysql_native_password (no caching_sha2). + * Verifies seq rewriting without RSA mediation. + */ +TEST_F(MySQLFilterTest, MySqlSslTerminateNativePasswordLoginOk) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting (standard, no caching_sha2_password). + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL request. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); + + // Client login (seq=2, rewritten to seq=1). + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + + // Server OK (seq=2, rewritten to seq=3). + std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * SSL Terminated: login followed by query execution. + * Verifies the filter works correctly after auth completes. + */ +TEST_F(MySQLFilterTest, MySqlSslTerminateLoginThenQuery) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Greeting + SSL + Login + OK (same as native password test above). + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + + std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + + // Now send a query — should be processed normally after auth. + Command mysql_cmd{}; + mysql_cmd.setCmd(Command::Cmd::Query); + mysql_cmd.setData("SELECT 1"); + Buffer::OwnedImpl query_data; + mysql_cmd.encode(query_data); + BufferHelper::encodeHdr(query_data, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_data, false)); + EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().queries_parsed_.value()); +} + +/** + * SSL Terminated: full auth RSA followed by query execution. + * End-to-end: greeting → SSL → login → AuthMore(0x04) → pw → RSA → OK → query. + */ +TEST_F(MySQLFilterTest, MySqlSslTerminateRsaThenQuery) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL + login. + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + Buffer::InstancePtr ssl_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + + Buffer::InstancePtr login_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*login_data, false)); + + // AuthMoreData(0x04). + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(RsaAuthState::WaitingClientPassword, filter_->getRsaAuthState()); + + // Client password → intercepted, request-public-key injected. + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)); + Buffer::InstancePtr pw_buf( + new Buffer::OwnedImpl(encodeRawPacket(std::string("secret") + '\0', 4))); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*pw_buf, false)); + + // PEM key → intercepted, encrypted password injected. + bssl::UniquePtr gen_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + EVP_PKEY_keygen_init(gen_ctx.get()); + EVP_PKEY_CTX_set_rsa_keygen_bits(gen_ctx.get(), 2048); + EVP_PKEY* raw_pkey = nullptr; + EVP_PKEY_keygen(gen_ctx.get(), &raw_pkey); + bssl::UniquePtr pkey(raw_pkey); + bssl::UniquePtr bio(BIO_new(BIO_s_mem())); + PEM_write_bio_PUBKEY(bio.get(), pkey.get()); + char* pem_data; + long pem_len = BIO_get_mem_data(bio.get(), &pem_data); + std::string pem_key(pem_data, pem_len); + + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)); + Buffer::InstancePtr key_buf(new Buffer::OwnedImpl( + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*key_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerResult, filter_->getRsaAuthState()); + + // Server OK (raw seq=6). + Buffer::InstancePtr ok_buf(new Buffer::OwnedImpl(encodeClientLoginResp(MYSQL_RESP_OK, 0, 6))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*ok_buf, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); + + // Query after RSA auth — verify seq numbers are correct and filter works. + Command mysql_cmd{}; + mysql_cmd.setCmd(Command::Cmd::Query); + mysql_cmd.setData("SELECT 1"); + Buffer::OwnedImpl query_data; + mysql_cmd.encode(query_data); + BufferHelper::encodeHdr(query_data, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_data, false)); + EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().queries_parsed_.value()); +} + +/** + * SSL Passthrough: server greeting with SSL → SSL pass-through → auth more data. + * Verifies that caching_sha2 auth more goes through unmodified in passthrough mode. + */ +TEST_F(MySQLFilterTest, MySqlSslPassthroughCachingSha2) { + initialize(); // downstream_ssl = DISABLE + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting (passes through). + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client SSL request → enters SslPt (passthrough) state. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*ssl_data, false)); + EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); + + // All further data is opaque (encrypted), filter just passes through. + Buffer::OwnedImpl encrypted_data("encrypted-login-packet-bytes"); + BufferHelper::encodeHdr(encrypted_data, CHALLENGE_SEQ_NUM + 1); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(encrypted_data, false)); + EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + + // RSA mediation never triggers. + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * SSL Terminated: startSecureTransport() fails → connection closed. + */ +TEST_F(MySQLFilterTest, MySqlSslTerminateStartTlsFails) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL request, but startSecureTransport fails. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(false)); + EXPECT_CALL(connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); +} + +/** + * No-SSL: caching_sha2 fast auth (0x03) without any SSL involved. + * Verifies the filter handles auth more data correctly without SSL termination. + */ +TEST_F(MySQLFilterTest, MySqlNoSslCachingSha2FastAuth) { + initialize(); // downstream_ssl = DISABLE + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client login (no SSL). + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + + // Server responds with fast auth success (0x03). + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FAST_AUTH_SUCCESS}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(MySQLSession::State::AuthSwitchMore, filter_->getSession().getState()); + // No RSA mediation without SSL termination. + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); + + // Server OK. + std::string ok_data = encodeClientLoginResp(MYSQL_RESP_OK, 0, 3); + Buffer::InstancePtr ok_buf(new Buffer::OwnedImpl(ok_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*ok_buf, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); +} + +/** + * No-SSL: caching_sha2 full auth (0x04) without SSL termination. + * The filter should NOT intercept — full auth is handled by client/server directly. + */ +TEST_F(MySQLFilterTest, MySqlNoSslCachingSha2FullAuth) { + initialize(); // downstream_ssl = DISABLE + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client login (no SSL). + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + + // Server responds with full auth required (0x04). + std::string auth_more_data = + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM); + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl(auth_more_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + // RSA mediation NOT triggered (downstream_ssl is DISABLE). + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); + + // Client sends request-public-key (0x02) — passes through to server. + std::string req_key = encodeRawPacket(std::string(1, MYSQL_REQUEST_PUBLIC_KEY), 3); + Buffer::InstancePtr req_key_buf(new Buffer::OwnedImpl(req_key)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*req_key_buf, false)); + + // No interception — filter stays inactive. + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * No-SSL: plain login followed by query. + * Basic end-to-end without any SSL. + */ +TEST_F(MySQLFilterTest, MySqlNoSslLoginThenQuery) { + initialize(); // downstream_ssl = DISABLE + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); + + std::string srv_resp_data = encodeClientLoginResp(MYSQL_RESP_OK); + Buffer::InstancePtr server_resp_data(new Buffer::OwnedImpl(srv_resp_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*server_resp_data, false)); + EXPECT_EQ(MySQLSession::State::Req, filter_->getSession().getState()); + + // Query. + Command mysql_cmd{}; + mysql_cmd.setCmd(Command::Cmd::Query); + mysql_cmd.setData("CREATE TABLE t (id INT)"); + Buffer::OwnedImpl query_data; + mysql_cmd.encode(query_data); + BufferHelper::encodeHdr(query_data, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_data, false)); + EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().queries_parsed_.value()); +} + +/** + * SSL REQUIRE mode: client that does NOT send SSL request gets rejected. + */ +TEST_F(MySQLFilterTest, MySqlSslRequireRejectsNonSslClient) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Server greeting. + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client sends login directly (no SSL request) — should be rejected. + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_CALL(connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); +} + +/** + * SSL REQUIRE mode: client that sends SSL request is accepted. + */ +TEST_F(MySQLFilterTest, MySqlSslRequireAcceptsSslClient) { + initialize(MySQLProxyProto::REQUIRE); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client sends SSL request — accepted. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + EXPECT_CALL(connection_, close(_)).Times(0); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + + // Client login after SSL — no rejection. + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); +} + +/** + * SSL ALLOW mode: client without SSL is accepted. + */ +TEST_F(MySQLFilterTest, MySqlSslAllowAcceptsNonSslClient) { + initialize(MySQLProxyProto::ALLOW); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client sends login directly (no SSL) — should be accepted in ALLOW mode. + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); + EXPECT_CALL(connection_, close(_)).Times(0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); + EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); +} + +/** + * SSL ALLOW mode: client with SSL initiates TLS, RSA mediation works. + */ +TEST_F(MySQLFilterTest, MySqlSslAllowWithSslClientRsaMediation) { + initialize(MySQLProxyProto::ALLOW); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Greeting with caching_sha2_password. + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // SSL request — accepted in ALLOW mode. + EXPECT_CALL(connection_, startSecureTransport()).WillOnce(testing::Return(true)); + Buffer::InstancePtr ssl_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*ssl_data, false)); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); + + // Client login. + Buffer::InstancePtr login_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM + 1))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*login_data, false)); + + // AuthMoreData(0x04) — full auth required, should trigger RSA mediation. + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(RsaAuthState::WaitingClientPassword, filter_->getRsaAuthState()); + + // Client password — intercepted, request-public-key injected. + EXPECT_CALL(filter_callbacks_, injectReadDataToFilterChain(_, false)); + Buffer::InstancePtr pw_buf( + new Buffer::OwnedImpl(encodeRawPacket(std::string("secret") + '\0', 4))); + EXPECT_EQ(Envoy::Network::FilterStatus::StopIteration, filter_->onData(*pw_buf, false)); + EXPECT_EQ(RsaAuthState::WaitingServerKey, filter_->getRsaAuthState()); +} + +/** + * SSL ALLOW mode: non-SSL client with caching_sha2 — NO RSA mediation. + * Client handles RSA directly with the server. + */ +TEST_F(MySQLFilterTest, MySqlSslAllowNonSslCachingSha2NoMediation) { + initialize(MySQLProxyProto::ALLOW); + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client sends login directly (no SSL). + Buffer::InstancePtr login_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM))); + EXPECT_CALL(connection_, close(_)).Times(0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*login_data, false)); + + // AuthMoreData(0x04) — should NOT trigger RSA mediation (no SSL was terminated). + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * SSL DISABLE mode: caching_sha2 full auth passes through without mediation. + */ +TEST_F(MySQLFilterTest, MySqlSslDisableCachingSha2FullAuthNoMediation) { + initialize(); // DISABLE + + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(ReturnRef(connection_)); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreetingCachingSha2(); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + Buffer::InstancePtr login_data( + new Buffer::OwnedImpl(encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*login_data, false)); + + // AuthMoreData(0x04) — no RSA mediation in DISABLE mode. + Buffer::InstancePtr auth_more(new Buffer::OwnedImpl( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*auth_more, false)); + EXPECT_EQ(RsaAuthState::Inactive, filter_->getRsaAuthState()); +} + +/** + * SSL DISABLE mode: SSL request passes through to server (passthrough behavior). + */ +TEST_F(MySQLFilterTest, MySqlSslDisableSslPassthrough) { + initialize(); // DISABLE + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); + + std::string greeting_data = encodeServerGreeting(MYSQL_PROTOCOL_10); + Buffer::InstancePtr greet_data(new Buffer::OwnedImpl(greeting_data)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data, false)); + + // Client sends SSL request — in DISABLE mode, it passes through (SslPt state). + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + Buffer::InstancePtr ssl_data(new Buffer::OwnedImpl(ssl_req)); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*ssl_data, false)); + EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); + EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); +} + } // namespace MySQLProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/contrib/mysql_proxy/filters/network/test/mysql_integration_test.cc b/contrib/mysql_proxy/filters/network/test/mysql_integration_test.cc index 282dc83bdb0ed..61745354e3980 100644 --- a/contrib/mysql_proxy/filters/network/test/mysql_integration_test.cc +++ b/contrib/mysql_proxy/filters/network/test/mysql_integration_test.cc @@ -13,6 +13,7 @@ #include "gtest/gtest.h" #include "mysql_test_utils.h" +using testing::Ge; namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -57,7 +58,7 @@ TEST_P(MySQLIntegrationTest, MySQLStatsNewSessionTest) { ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); } - test_server_->waitForCounterGe("mysql.mysql_stats.sessions", SESSIONS); + test_server_->waitForCounter("mysql.mysql_stats.sessions", Ge(SESSIONS)); } /** @@ -99,7 +100,7 @@ TEST_P(MySQLIntegrationTest, MySQLLoginTest) { tcp_client->close(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); - test_server_->waitForCounterGe("mysql.mysql_stats.login_attempts", 1); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); } @@ -149,7 +150,7 @@ TEST_P(MySQLIntegrationTest, DISABLED_MySQLUnitTestMultiClientsLoop) { } // Verify counters: CLIENT_NUM login attempts, no failures - test_server_->waitForCounterGe("mysql.mysql_stats.login_attempts", CLIENT_NUM); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(CLIENT_NUM)); EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_attempts")->value(), CLIENT_NUM); EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); } @@ -193,7 +194,7 @@ TEST_P(MySQLIntegrationTest, MySQLLoginFailTest) { tcp_client->close(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); - test_server_->waitForCounterGe("mysql.mysql_stats.login_attempts", 1); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 1); } diff --git a/contrib/mysql_proxy/filters/network/test/mysql_ssl_allow_test_config.yaml b/contrib/mysql_proxy/filters/network/test/mysql_ssl_allow_test_config.yaml new file mode 100644 index 0000000000000..b6f69f62f054d --- /dev/null +++ b/contrib/mysql_proxy/filters/network/test/mysql_ssl_allow_test_config.yaml @@ -0,0 +1,53 @@ +admin: + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "{}" + address: + socket_address: + address: "{}" + port_value: 0 +static_resources: + clusters: + name: cluster_0 + connect_timeout: 2s + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: 0 + listeners: + name: listener_0 + address: + socket_address: + address: "{}" + port_value: 0 + filter_chains: + - filters: + - name: mysql + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.mysql_proxy.v3.MySQLProxy + stat_prefix: mysql_stats + downstream_ssl: ALLOW + - name: tcp + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp_stats + cluster: cluster_0 + transport_socket: + name: starttls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig + cleartext_socket_config: + tls_socket_config: + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{}" + private_key: + filename: "{}" diff --git a/contrib/mysql_proxy/filters/network/test/mysql_ssl_disable_test_config.yaml b/contrib/mysql_proxy/filters/network/test/mysql_ssl_disable_test_config.yaml new file mode 100644 index 0000000000000..cf291957162bd --- /dev/null +++ b/contrib/mysql_proxy/filters/network/test/mysql_ssl_disable_test_config.yaml @@ -0,0 +1,40 @@ +admin: + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "{}" + address: + socket_address: + address: "{}" + port_value: 0 +static_resources: + clusters: + name: cluster_0 + connect_timeout: 2s + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: 0 + listeners: + name: listener_0 + address: + socket_address: + address: "{}" + port_value: 0 + filter_chains: + - filters: + - name: mysql + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.mysql_proxy.v3.MySQLProxy + stat_prefix: mysql_stats + - name: tcp + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp_stats + cluster: cluster_0 diff --git a/contrib/mysql_proxy/filters/network/test/mysql_ssl_integration_test.cc b/contrib/mysql_proxy/filters/network/test/mysql_ssl_integration_test.cc new file mode 100644 index 0000000000000..f017d4eb2381d --- /dev/null +++ b/contrib/mysql_proxy/filters/network/test/mysql_ssl_integration_test.cc @@ -0,0 +1,917 @@ +#include "envoy/extensions/transport_sockets/raw_buffer/v3/raw_buffer.pb.h" + +#include "source/common/buffer/buffer_impl.h" +#include "source/common/network/connection_impl.h" +#include "source/extensions/transport_sockets/raw_buffer/config.h" + +#include "test/integration/fake_upstream.h" +#include "test/integration/integration.h" +#include "test/integration/ssl_utility.h" +#include "test/integration/utility.h" +#include "test/test_common/network_utility.h" + +#include "contrib/mysql_proxy/filters/network/source/mysql_codec.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_clogin_resp.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_command.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_codec_greeting.h" +#include "contrib/mysql_proxy/filters/network/source/mysql_utils.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mysql_test_utils.h" +#include "openssl/evp.h" +#include "openssl/pem.h" + +using testing::Ge; +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace MySQLProxy { + +// Client connection that supports mid-stream TLS upgrade (startTLS). +// Adapted from test/extensions/transport_sockets/starttls/starttls_integration_test.cc. +class StartTlsClientConnection : public Network::ClientConnectionImpl { +public: + StartTlsClientConnection(Event::Dispatcher& dispatcher, + const Network::Address::InstanceConstSharedPtr& remote_address, + const Network::Address::InstanceConstSharedPtr& source_address, + Network::TransportSocketPtr&& transport_socket, + const Network::ConnectionSocket::OptionsSharedPtr& options) + : ClientConnectionImpl(dispatcher, remote_address, source_address, + std::move(transport_socket), options, nullptr) {} + + // Swap the raw transport socket for a TLS one and trigger the TLS handshake. + void upgradeToTls(Network::TransportSocketPtr&& tls_socket) { + transport_socket_ = std::move(tls_socket); + transport_socket_->setTransportSocketCallbacks(*this); + connecting_ = true; + ioHandle().activateFileEvents(Event::FileReadyType::Write); + } +}; + +class MySQLSSLIntegrationTest : public testing::TestWithParam, + public MySQLTestUtils, + public BaseIntegrationTest { + std::string mysqlSslConfig() { + return fmt::format( + fmt::runtime(TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( + "contrib/mysql_proxy/filters/network/test/mysql_ssl_require_test_config.yaml"))), + Platform::null_device_path, Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getAnyAddressString(GetParam()), + TestEnvironment::runfilesPath("test/config/integration/certs/servercert.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/serverkey.pem")); + } + +public: + MySQLSSLIntegrationTest() : BaseIntegrationTest(GetParam(), mysqlSslConfig()) { + skip_tag_extraction_rule_check_ = true; + } + + void initialize() override { + EXPECT_CALL(*mock_buffer_factory_, createBuffer_(_, _, _)) + .WillOnce( + Invoke([&](absl::AnyInvocable below_low, absl::AnyInvocable above_high, + absl::AnyInvocable above_overflow) -> Buffer::Instance* { + client_write_buffer_ = new NiceMock( + std::move(below_low), std::move(above_high), std::move(above_overflow)); + ON_CALL(*client_write_buffer_, move(_)) + .WillByDefault(Invoke(client_write_buffer_, &MockWatermarkBuffer::baseMove)); + ON_CALL(*client_write_buffer_, drain(_)) + .WillByDefault(Invoke(client_write_buffer_, &MockWatermarkBuffer::trackDrains)); + return client_write_buffer_; + })) + .WillOnce( + Invoke([&](absl::AnyInvocable below_low, absl::AnyInvocable above_high, + absl::AnyInvocable above_overflow) -> Buffer::Instance* { + return new Buffer::WatermarkBuffer(std::move(below_low), std::move(above_high), + std::move(above_overflow)); + })); + + // Create raw buffer and TLS transport socket factories. + auto raw_config = + std::make_unique(); + auto raw_factory = + std::make_unique(); + cleartext_context_ = Network::UpstreamTransportSocketFactoryPtr{ + raw_factory->createTransportSocketFactory(*raw_config, factory_context_).value()}; + + tls_context_manager_ = std::make_unique( + server_factory_context_); + tls_context_ = Ssl::createClientSslTransportSocketFactory({}, *tls_context_manager_, *api_); + + payload_reader_ = std::make_shared(*dispatcher_); + + BaseIntegrationTest::initialize(); + + // Create client connection with raw cleartext transport socket. + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("listener_0")); + conn_ = std::make_unique( + *dispatcher_, address, Network::Address::InstanceConstSharedPtr(), + cleartext_context_->createTransportSocket(nullptr, nullptr), nullptr); + conn_->enableHalfClose(true); + conn_->addConnectionCallbacks(connect_callbacks_); + conn_->addReadFilter(payload_reader_); + } + + // Upgrade the client connection to TLS. + // Wait for the filter to process the SSL request and call startSecureTransport() + // before initiating the client-side TLS handshake. + void upgradeClientToTls() { + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + conn_->upgradeToTls(tls_context_->createTransportSocket( + std::make_shared( + absl::string_view(""), std::vector(), std::vector()), + nullptr)); + connect_callbacks_.reset(); + while (!connect_callbacks_.connected() && !connect_callbacks_.closed()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + ASSERT_TRUE(connect_callbacks_.connected()); + } + + // Write data to client connection and wait for it to be sent. + void clientWrite(const std::string& data) { + uint64_t prev_drained = client_write_buffer_->bytesDrained(); + Buffer::OwnedImpl buf(data); + conn_->write(buf, false); + while (client_write_buffer_->bytesDrained() < prev_drained + data.length()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + } + + // Wait for specific payload from server (via Envoy) on the client side. + void clientWaitForData(const std::string& expected) { + payload_reader_->setDataToWaitFor(expected); + dispatcher_->run(Event::Dispatcher::RunType::Block); + } + + // Server greeting with caching_sha2_password and CLIENT_SSL. + std::string encodeServerGreetingCachingSha2() { + ServerGreeting greeting; + greeting.setProtocol(MYSQL_PROTOCOL_10); + greeting.setVersion(getVersion()); + greeting.setThreadId(MYSQL_THREAD_ID); + greeting.setAuthPluginData(getAuthPluginData20()); + greeting.setServerCap(CLIENT_PLUGIN_AUTH | CLIENT_SECURE_CONNECTION | CLIENT_SSL); + greeting.setServerCharset(MYSQL_SERVER_LANGUAGE); + greeting.setServerStatus(MYSQL_SERVER_STATUS); + greeting.setAuthPluginName("caching_sha2_password"); + Buffer::OwnedImpl buffer; + greeting.encode(buffer); + BufferHelper::encodeHdr(buffer, GREETING_SEQ_NUM); + return buffer.toString(); + } + + std::string encodeAuthMoreDataPacket(const std::vector& data, uint8_t seq) { + AuthMoreMessage auth_more; + auth_more.setAuthMoreData(data); + Buffer::OwnedImpl buffer; + auth_more.encode(buffer); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); + } + + std::string generateTestPemKey() { + bssl::UniquePtr gen_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + EVP_PKEY_keygen_init(gen_ctx.get()); + EVP_PKEY_CTX_set_rsa_keygen_bits(gen_ctx.get(), 2048); + EVP_PKEY* raw_pkey = nullptr; + EVP_PKEY_keygen(gen_ctx.get(), &raw_pkey); + bssl::UniquePtr pkey(raw_pkey); + bssl::UniquePtr bio(BIO_new(BIO_s_mem())); + PEM_write_bio_PUBKEY(bio.get(), pkey.get()); + char* pem_data; + long pem_len = BIO_get_mem_data(bio.get(), &pem_data); + // NOLINTNEXTLINE(modernize-return-braced-init-list) + return std::string(pem_data, pem_len); + } + + std::unique_ptr tls_context_manager_; + Network::UpstreamTransportSocketFactoryPtr tls_context_; + Network::UpstreamTransportSocketFactoryPtr cleartext_context_; + MockWatermarkBuffer* client_write_buffer_{nullptr}; + ConnectionStatusCallbacks connect_callbacks_; + std::unique_ptr conn_; + std::shared_ptr payload_reader_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, MySQLSSLIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +/** + * caching_sha2_password fast auth (cache hit) with SSL termination. + * + * Client ←TLS→ Envoy ←plaintext→ FakeUpstream + * greeting → SSL req → TLS upgrade → login → AuthMoreData(0x03) → OK + */ +TEST_P(MySQLSSLIntegrationTest, CachingSha2FastAuth) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // 1. Server greeting (cleartext, before TLS). + std::string greeting = encodeServerGreetingCachingSha2(); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // 2. Client sends SSL request (cleartext). + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + clientWrite(ssl_req); + // SSL request consumed by filter; filter calls startSecureTransport. + + // 3. Client upgrades to TLS. + upgradeClientToTls(); + + // 4. Client sends login over TLS (seq=2, rewritten to seq=1 for upstream). + std::string login = encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1); + clientWrite(login); + + // Upstream receives the rewritten login in cleartext. + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // 5. Server responds with fast auth success (seq=2, rewritten to seq=3 for client). + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FAST_AUTH_SUCCESS}, CHALLENGE_RESP_SEQ_NUM))); + + // 6. Server sends OK (seq=3, rewritten to seq=4 for client). + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK, 0, 3))); + + // Wait for client to receive both responses. + // Use a short sleep + non-block dispatch to let data flow through. + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); +} + +/** + * caching_sha2_password full auth (cache miss) with RSA mediation. + * + * greeting → SSL req → TLS → login → AuthMoreData(0x04) → client pw → + * [filter: request-public-key → PEM key → RSA encrypt] → OK + */ +TEST_P(MySQLSSLIntegrationTest, CachingSha2FullAuthRsaMediation) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // 1. Server greeting. + std::string greeting = encodeServerGreetingCachingSha2(); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // 2. SSL request + TLS upgrade. + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + + // 3. Client login over TLS. + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // 4. Server: full auth required (seq=2). + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + + // Wait for client to receive AuthMoreData (filter forwards it). + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // 5. Client sends cleartext password (seq=4 from client perspective). + std::string pw_payload = std::string("testpass") + '\0'; + Buffer::OwnedImpl pw_pkt; + BufferHelper::addString(pw_pkt, pw_payload); + BufferHelper::encodeHdr(pw_pkt, 4); + clientWrite(pw_pkt.toString()); + + // 6. Filter intercepts password, sends request-public-key (0x02, seq=3) to upstream. + size_t prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 5; }, + &upstream_data)); + + // Verify request-public-key packet. + std::string req_key = upstream_data.substr(prev_len); + EXPECT_EQ(static_cast(req_key[3]), 3u); // seq + EXPECT_EQ(static_cast(req_key[4]), MYSQL_REQUEST_PUBLIC_KEY); + + // 7. Server sends PEM public key (seq=4). + std::string pem_key = generateTestPemKey(); + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4))); + + // 8. Filter RSA-encrypts password and sends to upstream (seq=5, 256 bytes). + prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 4 + 256; }, + &upstream_data)); + + std::string enc_pkt = upstream_data.substr(prev_len); + uint32_t enc_len = static_cast(enc_pkt[0]) | (static_cast(enc_pkt[1]) << 8) | + (static_cast(enc_pkt[2]) << 16); + EXPECT_EQ(enc_len, 256u); // RSA-2048 ciphertext + EXPECT_EQ(static_cast(enc_pkt[3]), 5u); // seq + + // 9. Server sends OK (raw seq=6, rewritten to seq=5 for client). + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK, 0, 6))); + + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); +} + +/** + * Full auth — server rejects after RSA-encrypted password. + */ +TEST_P(MySQLSSLIntegrationTest, CachingSha2FullAuthRsaErr) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + std::string greeting = encodeServerGreetingCachingSha2(); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // Full auth required. + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Client password. + Buffer::OwnedImpl pw_pkt; + BufferHelper::addString(pw_pkt, std::string("wrongpw") + '\0'); + BufferHelper::encodeHdr(pw_pkt, 4); + clientWrite(pw_pkt.toString()); + + // Wait for request-public-key. + size_t prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 5; }, + &upstream_data)); + + // Send PEM key. + std::string pem_key = generateTestPemKey(); + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4))); + + // Wait for encrypted password. + prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 4 + 256; }, + &upstream_data)); + + // Server sends ERR (raw seq=6). + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_ERR, 0, 6))); + + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.login_failures", Ge(1)); +} + +/** + * SSL Terminated: basic login with native password (no caching_sha2), then query. + * Verifies seq rewriting works for the full lifecycle. + */ +TEST_P(MySQLSSLIntegrationTest, SslTerminateLoginThenQuery) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // Greeting (standard, not caching_sha2). + std::string greeting = encodeServerGreeting(MYSQL_PROTOCOL_10); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // SSL request + TLS upgrade. + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + + // Client login over TLS. + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // Server OK (seq=2, rewritten to seq=3 for client). + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK))); + + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Send a query (seq=0 after resetSeq). + Command mysql_cmd{}; + mysql_cmd.setCmd(Command::Cmd::Query); + mysql_cmd.setData("SELECT 1"); + Buffer::OwnedImpl query_buf; + mysql_cmd.encode(query_buf); + BufferHelper::encodeHdr(query_buf, 0); + clientWrite(query_buf.toString()); + + // Upstream should receive the query. + size_t prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() > prev_len; }, &upstream_data)); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.queries_parsed", Ge(1)); +} + +/** + * SSL Terminated: RSA mediation followed by query execution. + * Full lifecycle: greeting → SSL → login → AuthMore(0x04) → RSA → OK → query. + */ +TEST_P(MySQLSSLIntegrationTest, CachingSha2FullAuthRsaThenQuery) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // Greeting + SSL + login. + std::string greeting = encodeServerGreetingCachingSha2(); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // Full auth required → client password → RSA. + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + Buffer::OwnedImpl pw_pkt; + BufferHelper::addString(pw_pkt, std::string("testpass") + '\0'); + BufferHelper::encodeHdr(pw_pkt, 4); + clientWrite(pw_pkt.toString()); + + // Wait for request-public-key. + size_t prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 5; }, + &upstream_data)); + + // Send PEM key + wait for encrypted password. + std::string pem_key = generateTestPemKey(); + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4))); + prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 4 + 256; }, + &upstream_data)); + + // Server OK (raw seq=6). + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK, 0, 6))); + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Now send a query — verifies seq numbers are correct after RSA mediation. + Command mysql_cmd{}; + mysql_cmd.setCmd(Command::Cmd::Query); + mysql_cmd.setData("SELECT 1"); + Buffer::OwnedImpl query_buf; + mysql_cmd.encode(query_buf); + BufferHelper::encodeHdr(query_buf, 0); + clientWrite(query_buf.toString()); + + prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() > prev_len; }, &upstream_data)); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.queries_parsed", Ge(1)); +} + +// ============================================================================= +// DISABLE mode integration tests — plain TCP proxy, no SSL termination. +// ============================================================================= + +class MySQLDisableIntegrationTest : public testing::TestWithParam, + public MySQLTestUtils, + public BaseIntegrationTest { + std::string mysqlConfig() { + return fmt::format( + fmt::runtime(TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( + "contrib/mysql_proxy/filters/network/test/mysql_ssl_disable_test_config.yaml"))), + Platform::null_device_path, Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getAnyAddressString(GetParam())); + } + +public: + MySQLDisableIntegrationTest() : BaseIntegrationTest(GetParam(), mysqlConfig()) { + skip_tag_extraction_rule_check_ = true; + } + + void SetUp() override { BaseIntegrationTest::initialize(); } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, MySQLDisableIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +/** + * DISABLE mode: basic login, no SSL involved. + */ +TEST_P(MySQLDisableIntegrationTest, DisableBasicLogin) { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // Greeting. + std::string greeting = encodeServerGreeting(MYSQL_PROTOCOL_10); + ASSERT_TRUE(fake_upstream->write(greeting)); + tcp_client->waitForData(greeting, true); + + // Client login (no SSL). + std::string login = encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM); + ASSERT_TRUE(tcp_client->write(login)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData(login.length(), &upstream_data)); + EXPECT_EQ(login, upstream_data); + + // Server OK. + std::string ok_resp = encodeClientLoginResp(MYSQL_RESP_OK); + ASSERT_TRUE(fake_upstream->write(ok_resp)); + + std::string downstream(greeting); + downstream.append(ok_resp); + tcp_client->waitForData(downstream, true); + + tcp_client->close(); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); +} + +/** + * DISABLE mode: SSL request passes through to upstream (passthrough). + */ +TEST_P(MySQLDisableIntegrationTest, DisableSslPassthrough) { + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // Greeting. + std::string greeting = encodeServerGreeting(MYSQL_PROTOCOL_10); + ASSERT_TRUE(fake_upstream->write(greeting)); + tcp_client->waitForData(greeting, true); + + // Client sends SSL request — in DISABLE mode, it passes through. + std::string ssl_req = encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM); + ASSERT_TRUE(tcp_client->write(ssl_req)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData(ssl_req.length(), &upstream_data)); + // The SSL request should be forwarded unmodified. + EXPECT_EQ(ssl_req, upstream_data); + + tcp_client->close(); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.upgraded_to_ssl")->value(), 1); +} + +// ============================================================================= +// ALLOW mode integration tests — terminate SSL if client requests, accept cleartext. +// Uses the same StartTlsClientConnection as the REQUIRE tests. +// ============================================================================= + +class MySQLAllowIntegrationTest : public testing::TestWithParam, + public MySQLTestUtils, + public BaseIntegrationTest { + std::string mysqlSslConfig() { + return fmt::format( + fmt::runtime(TestEnvironment::readFileToStringForTest(TestEnvironment::runfilesPath( + "contrib/mysql_proxy/filters/network/test/mysql_ssl_allow_test_config.yaml"))), + Platform::null_device_path, Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getLoopbackAddressString(GetParam()), + Network::Test::getAnyAddressString(GetParam()), + TestEnvironment::runfilesPath("test/config/integration/certs/servercert.pem"), + TestEnvironment::runfilesPath("test/config/integration/certs/serverkey.pem")); + } + +public: + MySQLAllowIntegrationTest() : BaseIntegrationTest(GetParam(), mysqlSslConfig()) { + skip_tag_extraction_rule_check_ = true; + } + + void initialize() override { + EXPECT_CALL(*mock_buffer_factory_, createBuffer_(_, _, _)) + .WillOnce( + Invoke([&](absl::AnyInvocable below_low, absl::AnyInvocable above_high, + absl::AnyInvocable above_overflow) -> Buffer::Instance* { + client_write_buffer_ = new NiceMock( + std::move(below_low), std::move(above_high), std::move(above_overflow)); + ON_CALL(*client_write_buffer_, move(_)) + .WillByDefault(Invoke(client_write_buffer_, &MockWatermarkBuffer::baseMove)); + ON_CALL(*client_write_buffer_, drain(_)) + .WillByDefault(Invoke(client_write_buffer_, &MockWatermarkBuffer::trackDrains)); + return client_write_buffer_; + })) + .WillRepeatedly( + Invoke([](absl::AnyInvocable below_low, absl::AnyInvocable above_high, + absl::AnyInvocable above_overflow) -> Buffer::Instance* { + return new Buffer::WatermarkBuffer(std::move(below_low), std::move(above_high), + std::move(above_overflow)); + })); + + auto raw_config = + std::make_unique(); + auto raw_factory = + std::make_unique(); + cleartext_context_ = Network::UpstreamTransportSocketFactoryPtr{ + raw_factory->createTransportSocketFactory(*raw_config, factory_context_).value()}; + + tls_context_manager_ = std::make_unique( + server_factory_context_); + tls_context_ = Ssl::createClientSslTransportSocketFactory({}, *tls_context_manager_, *api_); + + payload_reader_ = std::make_shared(*dispatcher_); + + BaseIntegrationTest::initialize(); + + Network::Address::InstanceConstSharedPtr address = + Ssl::getSslAddress(version_, lookupPort("listener_0")); + conn_ = std::make_unique( + *dispatcher_, address, Network::Address::InstanceConstSharedPtr(), + cleartext_context_->createTransportSocket(nullptr, nullptr), nullptr); + conn_->enableHalfClose(true); + conn_->addConnectionCallbacks(connect_callbacks_); + conn_->addReadFilter(payload_reader_); + } + + void upgradeClientToTls() { + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + conn_->upgradeToTls(tls_context_->createTransportSocket( + std::make_shared( + absl::string_view(""), std::vector(), std::vector()), + nullptr)); + connect_callbacks_.reset(); + while (!connect_callbacks_.connected() && !connect_callbacks_.closed()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + ASSERT_TRUE(connect_callbacks_.connected()); + } + + void clientWrite(const std::string& data) { + uint64_t prev_drained = client_write_buffer_->bytesDrained(); + Buffer::OwnedImpl buf(data); + conn_->write(buf, false); + while (client_write_buffer_->bytesDrained() < prev_drained + data.length()) { + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + } + + void clientWaitForData(const std::string& expected) { + payload_reader_->setDataToWaitFor(expected); + dispatcher_->run(Event::Dispatcher::RunType::Block); + } + + std::string encodeServerGreetingCachingSha2() { + ServerGreeting greeting; + greeting.setProtocol(MYSQL_PROTOCOL_10); + greeting.setVersion(getVersion()); + greeting.setThreadId(MYSQL_THREAD_ID); + greeting.setAuthPluginData(getAuthPluginData20()); + greeting.setServerCap(CLIENT_PLUGIN_AUTH | CLIENT_SECURE_CONNECTION | CLIENT_SSL); + greeting.setServerCharset(MYSQL_SERVER_LANGUAGE); + greeting.setServerStatus(MYSQL_SERVER_STATUS); + greeting.setAuthPluginName("caching_sha2_password"); + Buffer::OwnedImpl buffer; + greeting.encode(buffer); + BufferHelper::encodeHdr(buffer, GREETING_SEQ_NUM); + return buffer.toString(); + } + + std::string encodeAuthMoreDataPacket(const std::vector& data, uint8_t seq) { + AuthMoreMessage auth_more; + auth_more.setAuthMoreData(data); + Buffer::OwnedImpl buffer; + auth_more.encode(buffer); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); + } + + std::string generateTestPemKey() { + bssl::UniquePtr gen_ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr)); + EVP_PKEY_keygen_init(gen_ctx.get()); + EVP_PKEY_CTX_set_rsa_keygen_bits(gen_ctx.get(), 2048); + EVP_PKEY* raw_pkey = nullptr; + EVP_PKEY_keygen(gen_ctx.get(), &raw_pkey); + bssl::UniquePtr pkey(raw_pkey); + bssl::UniquePtr bio(BIO_new(BIO_s_mem())); + PEM_write_bio_PUBKEY(bio.get(), pkey.get()); + char* pem_data; + long pem_len = BIO_get_mem_data(bio.get(), &pem_data); + // NOLINTNEXTLINE(modernize-return-braced-init-list) + return std::string(pem_data, pem_len); + } + + std::unique_ptr tls_context_manager_; + Network::UpstreamTransportSocketFactoryPtr tls_context_; + Network::UpstreamTransportSocketFactoryPtr cleartext_context_; + MockWatermarkBuffer* client_write_buffer_{nullptr}; + ConnectionStatusCallbacks connect_callbacks_; + std::unique_ptr conn_; + std::shared_ptr payload_reader_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, MySQLAllowIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); + +/** + * ALLOW mode: SSL client connects, terminates SSL, basic login OK. + */ +TEST_P(MySQLAllowIntegrationTest, AllowSslClientLogin) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + std::string greeting = encodeServerGreeting(MYSQL_PROTOCOL_10); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // SSL request + TLS upgrade — accepted in ALLOW mode. + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK))); + + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); +} + +/** + * ALLOW mode: non-SSL client connects in cleartext, login OK. + */ +TEST_P(MySQLAllowIntegrationTest, AllowNonSslClientLogin) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + std::string greeting = encodeServerGreeting(MYSQL_PROTOCOL_10); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // Client sends login directly (no SSL) — accepted in ALLOW mode. + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK))); + + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.upgraded_to_ssl")->value(), 0); +} + +/** + * ALLOW mode: SSL client with caching_sha2 full auth (RSA mediation). + */ +TEST_P(MySQLAllowIntegrationTest, AllowSslFullAuthRsaMediation) { + initialize(); + conn_->connect(); + + FakeRawConnectionPtr fake_upstream; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream)); + + // Greeting with caching_sha2_password. + std::string greeting = encodeServerGreetingCachingSha2(); + ASSERT_TRUE(fake_upstream->write(greeting)); + clientWaitForData(greeting); + + // SSL + login. + clientWrite(encodeClientLogin(CLIENT_SSL, "", CHALLENGE_SEQ_NUM)); + upgradeClientToTls(); + clientWrite(encodeClientLogin(CLIENT_PROTOCOL_41, "testuser", CHALLENGE_SEQ_NUM + 1)); + + std::string upstream_data; + ASSERT_TRUE(fake_upstream->waitForData([](const std::string& data) { return !data.empty(); }, + &upstream_data)); + + // Full auth required. + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket({MYSQL_CACHINGSHA2_FULL_AUTH_REQUIRED}, CHALLENGE_RESP_SEQ_NUM))); + timeSystem().advanceTimeWait(std::chrono::milliseconds(200)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Client password. + Buffer::OwnedImpl pw_pkt; + BufferHelper::addString(pw_pkt, std::string("testpass") + '\0'); + BufferHelper::encodeHdr(pw_pkt, 4); + clientWrite(pw_pkt.toString()); + + // Filter should inject request-public-key (0x02, seq=3). + size_t prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 5; }, + &upstream_data)); + + std::string req_key = upstream_data.substr(prev_len); + EXPECT_EQ(static_cast(req_key[3]), 3u); + EXPECT_EQ(static_cast(req_key[4]), MYSQL_REQUEST_PUBLIC_KEY); + + // Send PEM key. + std::string pem_key = generateTestPemKey(); + ASSERT_TRUE(fake_upstream->write( + encodeAuthMoreDataPacket(std::vector(pem_key.begin(), pem_key.end()), 4))); + + // Wait for encrypted password (256 bytes). + prev_len = upstream_data.length(); + ASSERT_TRUE(fake_upstream->waitForData( + [prev_len](const std::string& data) { return data.length() >= prev_len + 4 + 256; }, + &upstream_data)); + + // Server OK. + ASSERT_TRUE(fake_upstream->write(encodeClientLoginResp(MYSQL_RESP_OK, 0, 6))); + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + conn_->close(Network::ConnectionCloseType::FlushWrite); + ASSERT_TRUE(fake_upstream->waitForDisconnect()); + + test_server_->waitForCounter("mysql.mysql_stats.upgraded_to_ssl", Ge(1)); + test_server_->waitForCounter("mysql.mysql_stats.login_attempts", Ge(1)); + EXPECT_EQ(test_server_->counter("mysql.mysql_stats.login_failures")->value(), 0); +} + +} // namespace MySQLProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/mysql_proxy/filters/network/test/mysql_ssl_require_test_config.yaml b/contrib/mysql_proxy/filters/network/test/mysql_ssl_require_test_config.yaml new file mode 100644 index 0000000000000..e5536a29fc2fd --- /dev/null +++ b/contrib/mysql_proxy/filters/network/test/mysql_ssl_require_test_config.yaml @@ -0,0 +1,53 @@ +admin: + access_log: + - name: envoy.access_loggers.file + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: "{}" + address: + socket_address: + address: "{}" + port_value: 0 +static_resources: + clusters: + name: cluster_0 + connect_timeout: 2s + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: "{}" + port_value: 0 + listeners: + name: listener_0 + address: + socket_address: + address: "{}" + port_value: 0 + filter_chains: + - filters: + - name: mysql + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.mysql_proxy.v3.MySQLProxy + stat_prefix: mysql_stats + downstream_ssl: REQUIRE + - name: tcp + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + stat_prefix: tcp_stats + cluster: cluster_0 + transport_socket: + name: starttls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.starttls.v3.StartTlsConfig + cleartext_socket_config: + tls_socket_config: + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{}" + private_key: + filename: "{}" diff --git a/contrib/peak_ewma/filters/http/test/peak_ewma_filter_test.cc b/contrib/peak_ewma/filters/http/test/peak_ewma_filter_test.cc index c947915d48e90..b0945b99d55e8 100644 --- a/contrib/peak_ewma/filters/http/test/peak_ewma_filter_test.cc +++ b/contrib/peak_ewma/filters/http/test/peak_ewma_filter_test.cc @@ -8,10 +8,8 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using testing::_; using testing::NiceMock; using testing::Return; -using testing::ReturnRef; namespace Envoy { namespace Extensions { diff --git a/contrib/peak_ewma/load_balancing_policies/source/peak_ewma_lb.cc b/contrib/peak_ewma/load_balancing_policies/source/peak_ewma_lb.cc index 3945561499338..c00516c72952e 100644 --- a/contrib/peak_ewma/load_balancing_policies/source/peak_ewma_lb.cc +++ b/contrib/peak_ewma/load_balancing_policies/source/peak_ewma_lb.cc @@ -270,13 +270,15 @@ double PeakEwmaLoadBalancer::updateEwmaWithSample(double current_ewma, double ne void PeakEwmaLoadBalancer::processHostSamples(Upstream::HostConstSharedPtr /* host */, PeakEwmaHostLbPolicyData* data) { - if (!data) + if (!data) { return; + } // Get the range of new samples to process (atomic ring buffer). auto [last_processed, current_write] = data->getNewSampleRange(); - if (last_processed == current_write) + if (last_processed == current_write) { return; + } // If ring buffer was fully overwritten, skip to oldest valid slot. // Uses unsigned arithmetic (always correct since write_index_ only increments). diff --git a/contrib/peak_ewma/load_balancing_policies/test/BUILD b/contrib/peak_ewma/load_balancing_policies/test/BUILD index 745c6dc6cbc70..c3b6f4a101da2 100644 --- a/contrib/peak_ewma/load_balancing_policies/test/BUILD +++ b/contrib/peak_ewma/load_balancing_policies/test/BUILD @@ -30,7 +30,7 @@ envoy_cc_test( "//source/common/protobuf:utility_lib", "//test/common/stats:stat_test_utility_lib", "//test/mocks/runtime:runtime_mocks", - "//test/mocks/server:factory_context_mocks", + "//test/mocks/server:server_factory_context_mocks", "//test/mocks/upstream:upstream_mocks", "//test/test_common:utility_lib", "@envoy_api//contrib/envoy/extensions/load_balancing_policies/peak_ewma/v3alpha:pkg_cc_proto", diff --git a/contrib/peak_ewma/load_balancing_policies/test/config_test.cc b/contrib/peak_ewma/load_balancing_policies/test/config_test.cc index 8ecbebee8448e..041b8f79dc5a0 100644 --- a/contrib/peak_ewma/load_balancing_policies/test/config_test.cc +++ b/contrib/peak_ewma/load_balancing_policies/test/config_test.cc @@ -7,7 +7,7 @@ #include "test/common/stats/stat_test_utility.h" #include "test/mocks/common.h" #include "test/mocks/runtime/mocks.h" -#include "test/mocks/server/factory_context.h" +#include "test/mocks/server/server_factory_context.h" #include "test/mocks/upstream/cluster_info.h" #include "test/mocks/upstream/priority_set.h" #include "test/test_common/utility.h" diff --git a/contrib/peak_ewma/load_balancing_policies/test/peak_ewma_lb_host_lifecycle_test.cc b/contrib/peak_ewma/load_balancing_policies/test/peak_ewma_lb_host_lifecycle_test.cc index 30f5010b18476..0c78d75cb5e35 100644 --- a/contrib/peak_ewma/load_balancing_policies/test/peak_ewma_lb_host_lifecycle_test.cc +++ b/contrib/peak_ewma/load_balancing_policies/test/peak_ewma_lb_host_lifecycle_test.cc @@ -12,8 +12,6 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" -using testing::_; -using testing::Invoke; using testing::NiceMock; using testing::Return; using testing::ReturnRef; diff --git a/contrib/postgres_inspector/filters/listener/source/postgres_inspector.cc b/contrib/postgres_inspector/filters/listener/source/postgres_inspector.cc index 1ad535caab771..fb0a3e611b114 100644 --- a/contrib/postgres_inspector/filters/listener/source/postgres_inspector.cc +++ b/contrib/postgres_inspector/filters/listener/source/postgres_inspector.cc @@ -253,7 +253,7 @@ void Filter::extractMetadata(const StartupMessage& message) { } Protobuf::Any any; - any.PackFrom(typed); + std::ignore = any.PackFrom(typed); cb_->setDynamicTypedMetadata("envoy.postgres_inspector", any); ENVOY_LOG(debug, "postgres inspector: extracted metadata - user: {}, database: {}, app: {}", user_, database_, application_name_); diff --git a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_integration_test.cc b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_integration_test.cc index be5e355b818f4..891991a5ee843 100644 --- a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_integration_test.cc +++ b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_integration_test.cc @@ -14,6 +14,7 @@ #include "contrib/postgres_inspector/filters/listener/test/postgres_test_utils.h" #include "gtest/gtest.h" +using testing::Ge; namespace Envoy { namespace Extensions { namespace ListenerFilters { @@ -65,7 +66,7 @@ name: envoy.filters.listener.postgres_inspector envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy pg_config; pg_config.set_stat_prefix("tcp_postgres"); pg_config.set_cluster("cluster_postgres"); - pg_filter->mutable_typed_config()->PackFrom(pg_config); + std::ignore = pg_filter->mutable_typed_config()->PackFrom(pg_config); // Default filter chain for non-PostgreSQL traffic. auto* default_chain = listener->add_filter_chains(); @@ -75,7 +76,7 @@ name: envoy.filters.listener.postgres_inspector envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy default_config; default_config.set_stat_prefix("tcp_default"); default_config.set_cluster("cluster_default"); - default_filter->mutable_typed_config()->PackFrom(default_config); + std::ignore = default_filter->mutable_typed_config()->PackFrom(default_config); }); setUpstreamCount(2); @@ -169,8 +170,8 @@ TEST_P(PostgresInspectorIntegrationTest, DetectsPostgresPre17SSLRequest) { ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(1)); driver->close(); } @@ -186,8 +187,8 @@ TEST_P(PostgresInspectorIntegrationTest, DetectsPostgres17DirectSSL) { ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(1)); driver->close(); } @@ -203,8 +204,8 @@ TEST_P(PostgresInspectorIntegrationTest, DetectsPlaintextPostgresStartup) { ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_not_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_not_requested", Ge(1)); driver->close(); } @@ -220,7 +221,7 @@ TEST_P(PostgresInspectorIntegrationTest, DoesNotDetectNonPostgres) { ASSERT_TRUE(fake_upstreams_[1]->waitForRawConnection(upstream)); // Verify postgres_inspector did NOT detect PostgreSQL. - test_server_->waitForCounterGe("postgres_inspector.postgres_not_found", 1); + test_server_->waitForCounter("postgres_inspector.postgres_not_found", Ge(1)); driver->close(); } @@ -251,10 +252,10 @@ TEST_P(PostgresInspectorIntegrationTest, MultipleMixedConnectionsDetectedCorrect ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream4)); // Verify stats. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 3); - test_server_->waitForCounterGe("postgres_inspector.postgres_not_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 2); - test_server_->waitForCounterGe("postgres_inspector.ssl_not_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(3)); + test_server_->waitForCounter("postgres_inspector.postgres_not_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(2)); + test_server_->waitForCounter("postgres_inspector.ssl_not_requested", Ge(1)); driver1->close(); driver2->close(); diff --git a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_sni_integration_test.cc b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_sni_integration_test.cc index 002e7954a2a4a..0d229d59e04fe 100644 --- a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_sni_integration_test.cc +++ b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_sni_integration_test.cc @@ -15,6 +15,7 @@ #include "contrib/postgres_inspector/filters/listener/test/postgres_test_utils.h" #include "gtest/gtest.h" +using testing::Ge; namespace Envoy { namespace Extensions { namespace ListenerFilters { @@ -72,7 +73,7 @@ name: envoy.filters.listener.postgres_inspector envoy::extensions::filters::network::postgres_proxy::v3alpha::PostgresProxy pg_config; pg_config.set_stat_prefix("postgres_with_ssl"); pg_config.set_terminate_ssl(false); - pg_filter->mutable_typed_config()->PackFrom(pg_config); + std::ignore = pg_filter->mutable_typed_config()->PackFrom(pg_config); // Add TCP proxy for routing to backend. auto* tcp_filter = pg_chain->add_filters(); @@ -80,7 +81,7 @@ name: envoy.filters.listener.postgres_inspector envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy tcp_config; tcp_config.set_stat_prefix("tcp_postgres"); tcp_config.set_cluster("cluster_db1"); - tcp_filter->mutable_typed_config()->PackFrom(tcp_config); + std::ignore = tcp_filter->mutable_typed_config()->PackFrom(tcp_config); // Default chain for non-PostgreSQL traffic. auto* default_chain = listener->add_filter_chains(); @@ -90,7 +91,7 @@ name: envoy.filters.listener.postgres_inspector envoy::extensions::filters::network::tcp_proxy::v3::TcpProxy default_config; default_config.set_stat_prefix("tcp_default"); default_config.set_cluster("cluster_db2"); - default_filter->mutable_typed_config()->PackFrom(default_config); + std::ignore = default_filter->mutable_typed_config()->PackFrom(default_config); }); setUpstreamCount(2); @@ -172,11 +173,11 @@ TEST_P(PostgresInspectorWithProxyIntegrationTest, PostgresPre17DetectedAndRouted ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(1)); // Verify postgres_proxy is in the filter chain. - test_server_->waitForCounterGe("postgres.postgres_with_ssl.sessions", 1); + test_server_->waitForCounter("postgres.postgres_with_ssl.sessions", Ge(1)); driver->close(); } @@ -192,11 +193,11 @@ TEST_P(PostgresInspectorWithProxyIntegrationTest, Postgres17DirectSSLDetectedAnd ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(1)); // Verify postgres_proxy is in the filter chain. - test_server_->waitForCounterGe("postgres.postgres_with_ssl.sessions", 1); + test_server_->waitForCounter("postgres.postgres_with_ssl.sessions", Ge(1)); driver->close(); } @@ -212,8 +213,8 @@ TEST_P(PostgresInspectorWithProxyIntegrationTest, PlaintextPostgresDetectedAndRo ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream)); // Verify postgres_inspector detected the protocol. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 1); - test_server_->waitForCounterGe("postgres_inspector.ssl_not_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(1)); + test_server_->waitForCounter("postgres_inspector.ssl_not_requested", Ge(1)); driver->close(); } @@ -235,12 +236,12 @@ TEST_P(PostgresInspectorWithProxyIntegrationTest, MixedVersionConnectionsHandled ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream3)); // Verify stats demonstrate detection of all three types. - test_server_->waitForCounterGe("postgres_inspector.postgres_found", 3); - test_server_->waitForCounterGe("postgres_inspector.ssl_requested", 2); - test_server_->waitForCounterGe("postgres_inspector.ssl_not_requested", 1); + test_server_->waitForCounter("postgres_inspector.postgres_found", Ge(3)); + test_server_->waitForCounter("postgres_inspector.ssl_requested", Ge(2)); + test_server_->waitForCounter("postgres_inspector.ssl_not_requested", Ge(1)); // Verify postgres_proxy handled at least the SSL sessions. - test_server_->waitForCounterGe("postgres.postgres_with_ssl.sessions", 2); + test_server_->waitForCounter("postgres.postgres_with_ssl.sessions", Ge(2)); driver1->close(); driver2->close(); diff --git a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_test.cc b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_test.cc index 467c9cbf6cb95..71be47e16b8c2 100644 --- a/contrib/postgres_inspector/filters/listener/test/postgres_inspector_test.cc +++ b/contrib/postgres_inspector/filters/listener/test/postgres_inspector_test.cc @@ -16,14 +16,9 @@ #include "gtest/gtest.h" using testing::_; -using testing::DoAll; -using testing::InSequence; using testing::Invoke; -using testing::InvokeWithoutArgs; using testing::NiceMock; -using testing::Return; using testing::ReturnRef; -using testing::SaveArg; namespace Envoy { namespace Extensions { diff --git a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc index cb6f420e25165..e2a18918cf7d9 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_decoder_test.cc @@ -1,6 +1,8 @@ #include #include +#include + #include "contrib/postgres_proxy/filters/network/source/postgres_decoder.h" #include "contrib/postgres_proxy/filters/network/test/postgres_test_utils.h" @@ -712,7 +714,7 @@ class FakeBuffer : public Buffer::Instance { MOCK_METHOD(uint64_t, copyOutToSlices, (uint64_t size, Buffer::RawSlice* slices, uint64_t num_slice), (const, override)); MOCK_METHOD(void, drain, (uint64_t), (override)); - MOCK_METHOD(Buffer::RawSliceVector, getRawSlices, (absl::optional), (const, override)); + MOCK_METHOD(Buffer::RawSliceVector, getRawSlices, (std::optional), (const, override)); MOCK_METHOD(Buffer::RawSlice, frontSlice, (), (const, override)); MOCK_METHOD(Buffer::SliceDataPtr, extractMutableFrontSlice, (), (override)); MOCK_METHOD(uint64_t, length, (), (const, override)); diff --git a/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc index 3783ae7d62a73..a7449dd37e39b 100644 --- a/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc +++ b/contrib/postgres_proxy/filters/network/test/postgres_integration_test.cc @@ -19,6 +19,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +using testing::Eq; namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -121,7 +122,7 @@ TEST_P(BasicPostgresIntegrationTest, Login) { ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); // Make sure that the successful login bumped up the number of sessions. - test_server_->waitForCounterEq("postgres.postgres_stats.sessions", 1); + test_server_->waitForCounter("postgres.postgres_stats.sessions", Eq(1)); } INSTANTIATE_TEST_SUITE_P(IpVersions, BasicPostgresIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); @@ -180,7 +181,7 @@ TEST_P(DownstreamSSLPostgresIntegrationTest, TerminateSSL) { ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); // Make sure that the successful login bumped up the number of sessions. - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 1); + test_server_->waitForCounter("postgres.postgres_stats.sessions_terminated_ssl", Eq(1)); } // Test verifies that Postgres filter replies with error code upon @@ -244,7 +245,7 @@ TEST_P(DownstreamSSLWrongConfigPostgresIntegrationTest, TerminateSSLNoStartTlsTr tcp_client->waitForDisconnect(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 0); + test_server_->waitForCounter("postgres.postgres_stats.sessions_terminated_ssl", Eq(0)); } INSTANTIATE_TEST_SUITE_P(IpVersions, DownstreamSSLWrongConfigPostgresIntegrationTest, @@ -414,8 +415,8 @@ TEST_P(UpstreamSSLDisabledPostgresIntegrationTest, BasicConnectivityTest) { tcp_client->close(); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 0); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 0); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_success", Eq(0)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_failed", Eq(0)); } INSTANTIATE_TEST_SUITE_P(IpVersions, UpstreamSSLDisabledPostgresIntegrationTest, @@ -477,8 +478,8 @@ TEST_P(UpstreamSSLRequirePostgresIntegrationTest, ServerAgreesForSSLTest) { tcp_client->close(); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 1); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 0); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_success", Eq(1)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_failed", Eq(0)); } // Test verifies that postgres filter will not continue when upstream SSL @@ -513,8 +514,8 @@ TEST_P(UpstreamSSLRequirePostgresIntegrationTest, ServerDeniesSSLTest) { tcp_client->waitForDisconnect(); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 0); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 1); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_success", Eq(0)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_failed", Eq(1)); } INSTANTIATE_TEST_SUITE_P(IpVersions, UpstreamSSLRequirePostgresIntegrationTest, @@ -663,9 +664,9 @@ TEST_P(UpstreamAndDownstreamSSLIntegrationTest, ServerAgreesForSSL) { tcp_client->close(); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 1); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 1); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 0); + test_server_->waitForCounter("postgres.postgres_stats.sessions_terminated_ssl", Eq(1)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_success", Eq(1)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_failed", Eq(0)); } // Integration test when both downstream and upstream SSL is enabled. @@ -733,9 +734,9 @@ TEST_P(UpstreamAndDownstreamSSLIntegrationTest, ServerRejectsSSL) { tcp_client->waitForDisconnect(); ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_terminated_ssl", 1); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_success", 0); - test_server_->waitForCounterEq("postgres.postgres_stats.sessions_upstream_ssl_failed", 1); + test_server_->waitForCounter("postgres.postgres_stats.sessions_terminated_ssl", Eq(1)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_success", Eq(0)); + test_server_->waitForCounter("postgres.postgres_stats.sessions_upstream_ssl_failed", Eq(1)); } INSTANTIATE_TEST_SUITE_P(IpVersions, UpstreamAndDownstreamSSLIntegrationTest, diff --git a/contrib/qat/compression/qatzip/compressor/source/config.cc b/contrib/qat/compression/qatzip/compressor/source/config.cc index 446e10184baa0..f636fe5fa306b 100644 --- a/contrib/qat/compression/qatzip/compressor/source/config.cc +++ b/contrib/qat/compression/qatzip/compressor/source/config.cc @@ -122,6 +122,7 @@ QatzipCompressorLibraryFactory::createCompressorFactoryFromProto( const envoy::extensions::compression::qatzip::compressor::v3alpha::Qatzip&>( proto_config, context.messageValidationVisitor()); #ifdef QAT_DISABLED + static_cast(config); throw EnvoyException("X86_64 architecture is required for QAT."); #else return createCompressorFactoryFromProtoTyped(config, context); diff --git a/contrib/qat/compression/qatzstd/compressor/source/config.cc b/contrib/qat/compression/qatzstd/compressor/source/config.cc index 1813258a07ae9..8c0ce55a3404b 100644 --- a/contrib/qat/compression/qatzstd/compressor/source/config.cc +++ b/contrib/qat/compression/qatzstd/compressor/source/config.cc @@ -23,8 +23,7 @@ QatzstdCompressorFactory::QatzstdCompressorFactory( } } -QatzstdCompressorFactory::QatzstdThreadLocal::QatzstdThreadLocal() - : initialized_(false), sequenceProducerState_(nullptr) {} +QatzstdCompressorFactory::QatzstdThreadLocal::QatzstdThreadLocal() = default; QatzstdCompressorFactory::QatzstdThreadLocal::~QatzstdThreadLocal() { if (initialized_) { diff --git a/contrib/qat/compression/qatzstd/compressor/source/config.h b/contrib/qat/compression/qatzstd/compressor/source/config.h index 0142816075a57..b385fac6bf0f2 100644 --- a/contrib/qat/compression/qatzstd/compressor/source/config.h +++ b/contrib/qat/compression/qatzstd/compressor/source/config.h @@ -49,9 +49,10 @@ class QatzstdCompressorFactory : public Envoy::Compression::Compressor::Compress public Logger::Loggable { QatzstdThreadLocal(); ~QatzstdThreadLocal() override; + // NOLINTNEXTLINE(readability-identifier-naming) void* GetQATSession(); - bool initialized_; - void* sequenceProducerState_; + bool initialized_{false}; + void* sequenceProducerState_{nullptr}; }; const uint32_t compression_level_; const bool enable_checksum_; diff --git a/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.cc b/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.cc index b623d57320058..90b3950bc3364 100644 --- a/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.cc +++ b/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.cc @@ -10,11 +10,11 @@ QatzstdCompressorImpl::QatzstdCompressorImpl(uint32_t compression_level, bool en uint32_t strategy, uint32_t chunk_size, bool enable_qat_zstd, uint32_t qat_zstd_fallback_threshold, - void* sequenceProducerState) + void* sequence_producer_state) : ZstdCompressorImplBase(compression_level, enable_checksum, strategy, chunk_size), enable_qat_zstd_(enable_qat_zstd), qat_zstd_fallback_threshold_(qat_zstd_fallback_threshold), - sequenceProducerState_(sequenceProducerState), - input_ptr_{std::make_unique(chunk_size)}, input_len_(0), chunk_size_(chunk_size) { + sequence_producer_state_(sequence_producer_state), + input_ptr_{std::make_unique(chunk_size)}, chunk_size_(chunk_size) { size_t result; result = ZSTD_CCtx_setParameter(cctx_.get(), ZSTD_c_compressionLevel, compression_level_); RELEASE_ASSERT(!ZSTD_isError(result), ""); @@ -25,7 +25,7 @@ QatzstdCompressorImpl::QatzstdCompressorImpl(uint32_t compression_level, bool en compression_level, strategy, chunk_size, enable_qat_zstd, qat_zstd_fallback_threshold); if (enable_qat_zstd_) { /* register qatSequenceProducer */ - ZSTD_registerSequenceProducer(cctx_.get(), sequenceProducerState_, qatSequenceProducer); + ZSTD_registerSequenceProducer(cctx_.get(), sequence_producer_state_, qatSequenceProducer); result = ZSTD_CCtx_setParameter(cctx_.get(), ZSTD_c_enableSeqProducerFallback, 1); RELEASE_ASSERT(!ZSTD_isError(result), ""); } diff --git a/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.h b/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.h index 1960b13b7671d..a9cc157ab5f40 100644 --- a/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.h +++ b/contrib/qat/compression/qatzstd/compressor/source/qatzstd_compressor_impl.h @@ -23,7 +23,7 @@ class QatzstdCompressorImpl : public Envoy::Compression::Zstd::Compressor::ZstdC public: QatzstdCompressorImpl(uint32_t compression_level, bool enable_checksum, uint32_t strategy, uint32_t chunk_size, bool enable_qat_zstd, - uint32_t qat_zstd_fallback_threshold, void* sequenceProducerState); + uint32_t qat_zstd_fallback_threshold, void* sequence_producer_state); private: void compressPreprocess(Buffer::Instance& buffer, @@ -38,9 +38,9 @@ class QatzstdCompressorImpl : public Envoy::Compression::Zstd::Compressor::ZstdC bool enable_qat_zstd_; const uint32_t qat_zstd_fallback_threshold_; - void* sequenceProducerState_; + void* sequence_producer_state_; std::unique_ptr input_ptr_; - uint64_t input_len_; + uint64_t input_len_{0}; uint64_t chunk_size_; }; diff --git a/contrib/qat/private_key_providers/source/config.cc b/contrib/qat/private_key_providers/source/config.cc index aefc432b24811..9d638e5cb0d1d 100644 --- a/contrib/qat/private_key_providers/source/config.cc +++ b/contrib/qat/private_key_providers/source/config.cc @@ -37,6 +37,7 @@ QatPrivateKeyMethodFactory::createPrivateKeyMethodProviderInstance( *message, private_key_provider_context.messageValidationVisitor()); #ifdef QAT_DISABLED + static_cast(conf); throw EnvoyException("X86_64 architecture is required for QAT."); #else LibQatCryptoSharedPtr libqat = std::make_shared(); diff --git a/contrib/qat/private_key_providers/source/qat.h b/contrib/qat/private_key_providers/source/qat.h index 4e85a9da57a1e..69e96db9d56e7 100644 --- a/contrib/qat/private_key_providers/source/qat.h +++ b/contrib/qat/private_key_providers/source/qat.h @@ -47,14 +47,14 @@ class QatHandle : public Logger::Loggable { int getNodeAffinity(); int isDone(); - Thread::ThreadPtr polling_thread_{}; + Thread::ThreadPtr polling_thread_; Thread::MutexBasicLockable poll_lock_{}; Thread::CondVar qat_thread_cond_{}; private: CpaInstanceHandle handle_; CpaInstanceInfo2 info_; - LibQatCryptoSharedPtr libqat_{}; + LibQatCryptoSharedPtr libqat_; int users_{}; bool done_{}; }; @@ -75,7 +75,7 @@ class QatSection : public Logger::Loggable { Cpa16U num_instances_{}; std::vector qat_handles_; int next_handle_{}; - LibQatCryptoSharedPtr libqat_{}; + LibQatCryptoSharedPtr libqat_; }; /** @@ -96,7 +96,7 @@ class QatManager : public std::enable_shared_from_this, bool checkQatDevice(); private: - LibQatCryptoSharedPtr libqat_{}; + LibQatCryptoSharedPtr libqat_; bool qat_is_supported_{true}; }; diff --git a/contrib/qat/private_key_providers/source/qat_private_key_provider.h b/contrib/qat/private_key_providers/source/qat_private_key_provider.h index 7636430233c92..c24df1b4ad3f6 100644 --- a/contrib/qat/private_key_providers/source/qat_private_key_provider.h +++ b/contrib/qat/private_key_providers/source/qat_private_key_provider.h @@ -29,7 +29,7 @@ class QatPrivateKeyConnection { private: Ssl::PrivateKeyConnectionCallbacks& cb_; Event::Dispatcher& dispatcher_; - Event::FileEventPtr ssl_async_event_{}; + Event::FileEventPtr ssl_async_event_; QatHandle& handle_; bssl::UniquePtr pkey_; }; @@ -51,12 +51,12 @@ class QatPrivateKeyMethodProvider : public virtual Ssl::PrivateKeyMethodProvider Ssl::BoringSslPrivateKeyMethodSharedPtr getBoringSslPrivateKeyMethod() override; private: - Ssl::BoringSslPrivateKeyMethodSharedPtr method_{}; + Ssl::BoringSslPrivateKeyMethodSharedPtr method_; std::shared_ptr manager_; std::shared_ptr section_; Api::Api& api_; bssl::UniquePtr pkey_; - LibQatCryptoSharedPtr libqat_{}; + LibQatCryptoSharedPtr libqat_; bool initialized_{}; }; diff --git a/contrib/reverse_tunnel_reporter/source/BUILD b/contrib/reverse_tunnel_reporter/source/BUILD new file mode 100644 index 0000000000000..6890914b2f78a --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/BUILD @@ -0,0 +1,36 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_contrib_extension", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "reverse_tunnel_event_types", + hdrs = [ + "reverse_tunnel_event_types.h", + ], + deps = [ + "//envoy/common:pure_lib", + "//envoy/common:time_interface", + "//envoy/config:typed_config_interface", + "//envoy/extensions/bootstrap/reverse_tunnel:reverse_tunnel_reporter_lib", + "//envoy/server:factory_context_interface", + "//source/common/common:fmt_lib", + "//source/common/config:utility_lib", + "//source/common/protobuf", + "//source/common/protobuf:message_validator_lib", + ], +) + +envoy_cc_contrib_extension( + name = "config", + deps = [ + "//contrib/reverse_tunnel_reporter/source/clients:clients_lib", + "//contrib/reverse_tunnel_reporter/source/reporters:reporters_lib", + ], +) diff --git a/contrib/reverse_tunnel_reporter/source/clients/BUILD b/contrib/reverse_tunnel_reporter/source/clients/BUILD new file mode 100644 index 0000000000000..e8886c7f0e7e5 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/BUILD @@ -0,0 +1,16 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "clients_lib", + deps = [ + "//contrib/reverse_tunnel_reporter/source/clients/grpc_client:grpc_client_lib", + ], +) diff --git a/contrib/reverse_tunnel_reporter/source/clients/grpc_client/BUILD b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/BUILD new file mode 100644 index 0000000000000..17adeffe19274 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/BUILD @@ -0,0 +1,29 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "grpc_client_lib", + srcs = [ + "client.cc", + "factory.cc", + ], + hdrs = [ + "client.h", + "factory.h", + ], + deps = [ + "//contrib/reverse_tunnel_reporter/source:reverse_tunnel_event_types", + "//envoy/registry", + "//source/common/common:logger_lib", + "//source/common/grpc:typed_async_client_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client:pkg_cc_proto", + ], +) diff --git a/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.cc b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.cc new file mode 100644 index 0000000000000..71b2c6b5ca726 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.cc @@ -0,0 +1,283 @@ +#include "contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +GrpcClientConfig::GrpcClientConfig(const GrpcConfigProto& proto_config) + : stat_prefix(PROTOBUF_GET_STRING_OR_DEFAULT(proto_config, stat_prefix, + "reverse_tunnel_reporter_client.grpc_client")), + cluster(proto_config.cluster()), + send_interval(PROTOBUF_GET_MS_OR_DEFAULT(proto_config, default_send_interval, 5000)), + connect_retry_interval( + PROTOBUF_GET_MS_OR_DEFAULT(proto_config, connect_retry_interval, 5000)), + max_retries(proto_config.max_retries() ? proto_config.max_retries() : 5), + max_buffer(proto_config.max_buffer_count() ? proto_config.max_buffer_count() : 1000000) {} + +GrpcClient::GrpcClient(Server::Configuration::ServerFactoryContext& context, + const GrpcConfigProto& config) + : context_{context}, config_{config}, + service_method_{*Protobuf::DescriptorPool::generated_pool()->FindMethodByName( + "envoy.extensions.reverse_tunnel_reporters.v3alpha.clients.grpc_client" + ".ReverseTunnelReportingService.StreamReverseTunnels")}, + stats_(context_, config_.stat_prefix, config_.cluster) { + ENVOY_LOG(info, + "GrpcClient: constructed: cluster={}, send_interval={}ms, retry_interval={}ms, " + "max_retries={}, max_buffer={}", + config_.cluster, config_.send_interval.count(), config_.connect_retry_interval.count(), + config_.max_retries, config_.max_buffer); + + send_timer_ = context.mainThreadDispatcher().createTimer([this]() { send(false); }); + + retry_timer_ = context.mainThreadDispatcher().createTimer([this]() { connect(); }); + + stats_.send_interval_gauge_.set(config_.send_interval.count()); +} + +void GrpcClient::onServerInitialized(ReverseTunnelReporterWithState* reporter) { + reporter_ = reporter; + + envoy::config::core::v3::GrpcService grpc_service; + grpc_service.mutable_envoy_grpc()->set_cluster_name(config_.cluster); + + auto thread_local_cluster = context_.clusterManager().getThreadLocalCluster(config_.cluster); + if (!thread_local_cluster) { + ENVOY_LOG(error, "GrpcClient: cluster '{}' not found, cannot initialize", config_.cluster); + return; + } + + auto result = context_.clusterManager().grpcAsyncClientManager().getOrCreateRawAsyncClient( + grpc_service, thread_local_cluster->info()->statsScope(), false); + if (!result.ok()) { + ENVOY_LOG(error, "GrpcClient: failed to create gRPC async client: {}", + result.status().message()); + return; + } + + async_client_ = Grpc::AsyncClient(result.value()); + ENVOY_LOG(info, "GrpcClient: initialized: cluster={}", config_.cluster); + + initialized_ = true; + connect(); +} + +void GrpcClient::receiveEvents(ReverseTunnelEvent::TunnelUpdates updates) { + // Either we errored out of the initialized -> prevent infinite growth. + // Or the onServerInitialized has not been called yet -> no worries we will do a full push on + // connect. + if (!initialized_) { + ENVOY_LOG(debug, "GrpcClient: not initialized, cannot receive events"); + return; + } + + if ((updates.size() + queued_updates_.size()) > config_.max_buffer) { + ENVOY_LOG(error, + "GrpcClient: buffer overflow: cluster={}, queued={}, incoming={}, max_buffer={}", + config_.cluster, queued_updates_.size(), updates.size(), config_.max_buffer); + + stats_.events_dropped_counter_.add(updates.size()); + + // Only disconnect if the stream is alive. If already disconnected, calling disconnect() + // would re-arm the retry timer, delaying the reconnect that is already scheduled. + if (stream_ != nullptr) { + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted, + GrpcDisconnectionReason::DisconnectReason::BUFFER_OVERFLOW)) + .inc(); + disconnect(); + } + return; + } + + const auto incoming_conns = updates.connections.size(); + const auto incoming_disconns = updates.disconnections.size(); + stats_.queued_updates_counter_.add(updates.size()); + queued_updates_ += std::move(updates); + ENVOY_LOG(debug, "GrpcClient: enqueued: cluster={}, +={}, -={}, queued_now={}", config_.cluster, + incoming_conns, incoming_disconns, queued_updates_.size()); +} + +void GrpcClient::onReceiveMessage(Grpc::ResponsePtr&& message) { + const auto resp_nonce = message->request_nonce(); + + if (message->has_error_detail()) { + ENVOY_LOG(error, "GrpcClient: NACK: cluster={}, nonce={}", config_.cluster, resp_nonce); + + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::Aborted, + GrpcDisconnectionReason::DisconnectReason::NACK_RECEIVED)) + .inc(); + return disconnect(); + } + + // A server cannot ACK a nonce we never sent. If this fires the server has a bug. + ASSERT( + resp_nonce <= nonce_current_, + fmt::format("server acked nonce {} but we only sent up to {}", resp_nonce, nonce_current_)); + + // Valid ACK: must be newer than the last acked watermark and within what we've sent. + if (resp_nonce > nonce_acked_ && resp_nonce <= nonce_current_) { + stats_.acks_received_counter_.inc(); + nonce_acked_ = resp_nonce; + stats_.nonce_acked_gauge_.set(nonce_acked_); + + // The server may dynamically adjust our send cadence via report_interval in each ACK. + // The new interval will take effect from the next schedule onwards the already scheduled send + // is not affected. This is sticky and stays across disconnects also. + config_.send_interval = std::chrono::milliseconds( + PROTOBUF_GET_MS_OR_DEFAULT(*message, report_interval, config_.send_interval.count())); + stats_.send_interval_gauge_.set(config_.send_interval.count()); + + ENVOY_LOG(debug, "GrpcClient: ACK: cluster={}, nonce={}", config_.cluster, resp_nonce); + } else { + ENVOY_LOG(error, "GrpcClient: out-of-order ACK: cluster={}, nonce={}, expected_range=[{}, {}]", + config_.cluster, resp_nonce, nonce_acked_ + 1, nonce_current_); + stats_.out_of_order_acks_counter_.inc(); + } +} + +void GrpcClient::onRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message) { + // Even a graceful close is unexpected — the server should keep the stream open indefinitely. + if (status == Grpc::Status::WellKnownGrpcStatus::Ok) { + ENVOY_LOG(error, "GrpcClient: remote close (ok): cluster={}, message={}", config_.cluster, + message); + } else { + ENVOY_LOG(error, "GrpcClient: remote close: cluster={}, status={}, message={}", config_.cluster, + status, message); + } + + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(status, GrpcDisconnectionReason::DisconnectReason::REMOTE_CLOSE)) + .inc(); + disconnect(); +} + +void GrpcClient::connect() { + ENVOY_LOG(info, "GrpcClient: connecting: cluster={}", config_.cluster); + + stream_ = async_client_.start(service_method_, *this, Http::AsyncClient::StreamOptions()); + if (stream_ == nullptr) { + ENVOY_LOG(error, "GrpcClient: stream creation failed: cluster={}", config_.cluster); + + stats_ + .getCounter( + stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::Internal, + GrpcDisconnectionReason::DisconnectReason::STREAM_CREATION_FAILED)) + .inc(); + return disconnect(); + } + + // New stream, new nonce epoch. Stale nonces from the previous stream are meaningless. + nonce_acked_ = nonce_current_ = 0; + stats_.nonce_current_gauge_.set(0); + stats_.nonce_acked_gauge_.set(0); + + stats_.connection_attempts_counter_.inc(); + ENVOY_LOG(info, "GrpcClient: connected: cluster={}", config_.cluster); + send(true); +} + +void GrpcClient::disconnect() { + if (stream_ != nullptr) { + stream_.resetStream(); + stream_ = nullptr; + } + + // Stop the send loop — no point sending on a dead stream. The retry timer will reconnect. + send_timer_->disableTimer(); + setTimer(retry_timer_, config_.connect_retry_interval); + ENVOY_LOG(debug, "GrpcClient: disconnect, scheduled reconnect: cluster={}, retry_in_ms={}", + config_.cluster, config_.connect_retry_interval.count()); +} + +void GrpcClient::send(bool full_push) { + ASSERT(stream_ != nullptr); + // Too many in-flight unacked messages — the server is likely dead or stuck. Disconnect + // and let the retry timer establish a fresh stream. + if ((nonce_current_ - nonce_acked_) > config_.max_retries) { + ENVOY_LOG(error, "GrpcClient: too many unacked requests: cluster={}, nonce={}", config_.cluster, + nonce_current_); + + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::DeadlineExceeded, + GrpcDisconnectionReason::DisconnectReason::MAX_RETRIES_EXCEEDED)) + .inc(); + return disconnect(); + } + + ENVOY_LOG(debug, "GrpcClient: sending: cluster={}, full_push={}, queued_now={}", config_.cluster, + full_push, queued_updates_.size()); + stats_.send_attempts_counter_.inc(); + stream_.sendMessage(constructMessage(full_push), false); + + setTimer(send_timer_, config_.send_interval); +} + +StreamTunnelsReq GrpcClient::constructMessage(bool full_push) { + // Full push replaces the pending diff queue with a complete snapshot from the reporter. + // Any queued diffs are stale at this point because the full snapshot supersedes them. + if (full_push) { + stats_.events_dropped_counter_.add(queued_updates_.size()); + queued_updates_ = ReverseTunnelEvent::TunnelUpdates{{reporter_->getAllConnections()}, {}}; + stats_.queued_updates_counter_.add(queued_updates_.size()); + ENVOY_LOG(info, "GrpcClient: full_push queued: cluster={}, queued_now={}", config_.cluster, + queued_updates_.size()); + } + + StreamTunnelsReq message; + + auto* node = message.mutable_node(); + node->set_id(context_.localInfo().nodeName()); + node->set_cluster(context_.localInfo().clusterName()); + + auto* added_tunnels = message.mutable_added_tunnels(); + for (auto& conn : queued_updates_.connections) { + auto* new_tunnel = added_tunnels->Add(); + new_tunnel->set_name(ReverseTunnelEvent::getName(conn->node_id)); + TimestampUtil::systemClockToTimestamp(conn->created_at, *new_tunnel->mutable_created_at()); + + auto* tunnel_id = new_tunnel->mutable_identity(); + tunnel_id->set_tenant_id(conn->tenant_id); + tunnel_id->set_cluster_id(conn->cluster_id); + tunnel_id->set_node_id(conn->node_id); + } + + auto* removed_tunnels = message.mutable_removed_tunnel_names(); + for (auto& disconn : queued_updates_.disconnections) { + *removed_tunnels->Add() = disconn->name; + } + + message.set_full_push(full_push); + message.set_nonce(++nonce_current_); + stats_.nonce_current_gauge_.set(nonce_current_); + + ENVOY_LOG(debug, + "GrpcClient: built request: cluster={}, full_push={}, add={}, remove={}, nonce={}", + config_.cluster, full_push, queued_updates_.connections.size(), + queued_updates_.disconnections.size(), message.nonce()); + + stats_.sent_accepted_cnt_counter_.add(queued_updates_.connections.size()); + stats_.sent_removed_cnt_counter_.add(queued_updates_.disconnections.size()); + queued_updates_.clear(); + + return message; +} + +void GrpcClient::setTimer(Event::TimerPtr& timer, const std::chrono::milliseconds& ms) { + if (timer->enabled()) { + timer->disableTimer(); + } + + timer->enableTimer(ms); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h new file mode 100644 index 0000000000000..65283539be166 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h @@ -0,0 +1,229 @@ +#pragma once + +#include +#include +#include + +#include "envoy/event/timer.h" +#include "envoy/grpc/async_client.h" + +#include "source/common/common/logger.h" +#include "source/common/grpc/typed_async_client.h" +#include "source/common/protobuf/utility.h" +#include "source/common/stats/symbol_table.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/grpc_client.pb.h" +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/stream_reverse_tunnels.pb.h" +#include "contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +namespace GrpcDisconnectionReason { + +#define ITEMS(X) \ + X(BUFFER_OVERFLOW, buffer_overflow) \ + X(MAX_RETRIES_EXCEEDED, max_retries_exceeded) \ + X(NACK_RECEIVED, nack_received) \ + X(REMOTE_CLOSE, remote_close) \ + X(STREAM_CREATION_FAILED, stream_creation_failed) + +#define ENUM_DECLARE(name, str) name, +enum class DisconnectReason { ITEMS(ENUM_DECLARE) COUNT }; +#undef ENUM_DECLARE + +#define ENUM_STRING(name, str) #str, +constexpr std::array(DisconnectReason::COUNT)> + DisconnectReasonStrings = {ITEMS(ENUM_STRING)}; +#undef ENUM_STRING + +constexpr absl::string_view toString(DisconnectReason r) { + return DisconnectReasonStrings[static_cast(r)]; +} + +} // namespace GrpcDisconnectionReason + +using GrpcConfigProto = + envoy::extensions::reverse_tunnel_reporters::v3alpha::clients::grpc_client::GrpcClientConfig; +using StreamTunnelsReq = envoy::extensions::reverse_tunnel_reporters::v3alpha::clients:: + grpc_client::StreamReverseTunnelsRequest; +using StreamTunnelsResp = envoy::extensions::reverse_tunnel_reporters::v3alpha::clients:: + grpc_client::StreamReverseTunnelsResponse; + +/// Parsed and validated configuration from the GrpcClientConfig proto, with defaults applied. +struct GrpcClientConfig { + std::string stat_prefix; + std::string cluster; + std::chrono::milliseconds send_interval; + std::chrono::milliseconds connect_retry_interval; + uint32_t max_retries; + uint32_t max_buffer; + + explicit GrpcClientConfig(const GrpcConfigProto& config); +}; + +/// Bidirectional gRPC streaming client that reports reverse-tunnel connection +/// state to a remote ReverseTunnelReportingService. +/// +/// Protocol: +/// - On connect (and every reconnect) the client does a full push of all +/// known connections obtained from the reporter. +/// - Between connects the client sends incremental diffs (new connections / +/// removals) on a periodic send timer. +/// - Each request carries an incrementing nonce. The server ACKs by echoing +/// the nonce; a NACK carries an error_detail. If too many nonces remain +/// unacked the client disconnects and reconnects. +/// - The server may adjust the send interval via report_interval in its ACK. +/// +/// Lifecycle: constructed by GrpcClientFactory, initialized via +/// onServerInitialized() which creates the gRPC channel and opens the first +/// stream and starts the send cycle. Empty messages are also sent (considered as heartbeat). +class GrpcClient : public ReverseTunnelReporterClient, + public Logger::Loggable, + public Grpc::AsyncStreamCallbacks { +public: + GrpcClient(Server::Configuration::ServerFactoryContext& context, const GrpcConfigProto& config); + + // ReverseTunnelReporterClient overrides + void onServerInitialized(ReverseTunnelReporterWithState* reporter) override; + + void receiveEvents(ReverseTunnelEvent::TunnelUpdates updates) override; + + // RawAsyncStreamCallbacks overrides + void onCreateInitialMetadata(Http::RequestHeaderMap&) override {} + + void onReceiveInitialMetadata(Http::ResponseHeaderMapPtr&&) override {} + + void onReceiveTrailingMetadata(Http::ResponseTrailerMapPtr&&) override {} + + void onReceiveMessage(Grpc::ResponsePtr&& message) override; + + void onRemoteClose(Grpc::Status::GrpcStatus status, const std::string& message) override; + +private: + // Actions + void connect(); + + void disconnect(); + + void send(bool full_push); + + // Helpers + StreamTunnelsReq constructMessage(bool full_push); + + void setTimer(Event::TimerPtr& timer, const std::chrono::milliseconds& ms); + + // config + Server::Configuration::ServerFactoryContext& context_; + GrpcClientConfig config_; + ReverseTunnelReporterWithState* reporter_{nullptr}; + + // State management + ReverseTunnelEvent::TunnelUpdates queued_updates_; + int64_t nonce_current_{0}; // Monotonically increasing, bumped on every sendMessage. + int64_t nonce_acked_{0}; // High watermark of server-acknowledged nonces. + // Guards against processing events when onServerInitialized() failed (cluster not + // found, client creation error). Without this the client silently queues to nowhere. + bool initialized_{false}; + + // grpc client and stream requirements + Grpc::AsyncClient async_client_; + Grpc::AsyncStream stream_; + const Protobuf::MethodDescriptor& service_method_; + + // timers + Event::TimerPtr retry_timer_; + Event::TimerPtr send_timer_; + + struct GrpcClientStats { + GrpcClientStats(Server::Configuration::ServerFactoryContext& context, + const std::string& stat_prefix, const std::string& cluster_name) + : context_{context}, stat_name_pool_(context.scope().symbolTable()), + stat_prefix_(stat_name_pool_.add(stat_prefix)), + + disconnects_(stat_name_pool_.add("disconnects")), + + status_code_(stat_name_pool_.add("status_code")), + disconnect_reason_(stat_name_pool_.add("disconnect_reason")), + cluster_label_(stat_name_pool_.add("cluster")), + cluster_value_(stat_name_pool_.add(cluster_name)), + + connection_attempts_counter_( + getCounter(stat_name_pool_.add("connection_attempts"), getTags())), + acks_received_counter_(getCounter(stat_name_pool_.add("acks_received"), getTags())), + send_attempts_counter_(getCounter(stat_name_pool_.add("send_attempts"), getTags())), + sent_accepted_cnt_counter_( + getCounter(stat_name_pool_.add("sent_accepted_cnt"), getTags())), + sent_removed_cnt_counter_(getCounter(stat_name_pool_.add("sent_removed_cnt"), getTags())), + events_dropped_counter_(getCounter(stat_name_pool_.add("events_dropped"), getTags())), + queued_updates_counter_(getCounter(stat_name_pool_.add("queued_events"), getTags())), + out_of_order_acks_counter_( + getCounter(stat_name_pool_.add("out_of_order_acks"), getTags())), + + send_interval_gauge_(getGauge(stat_name_pool_.add("send_interval"), getTags())), + nonce_current_gauge_(getGauge(stat_name_pool_.add("nonce_current"), getTags())), + nonce_acked_gauge_(getGauge(stat_name_pool_.add("nonce_acked"), getTags())) {} + + Stats::StatNameTagVector getTags() { + return Stats::StatNameTagVector{ + {cluster_label_, cluster_value_}, + }; + } + + Stats::StatNameTagVector getTags(Grpc::Status::GrpcStatus status, + GrpcDisconnectionReason::DisconnectReason reason) { + Stats::StatName status_value = stat_name_pool_.add(std::to_string(status)); + Stats::StatName reason_value = stat_name_pool_.add(GrpcDisconnectionReason::toString(reason)); + + return Stats::StatNameTagVector{ + {cluster_label_, cluster_value_}, + {status_code_, status_value}, + {disconnect_reason_, reason_value}, + }; + } + + Stats::Counter& getCounter(const Stats::StatName& name, Stats::StatNameTagVector&& tags) { + return Stats::Utility::counterFromStatNames(context_.scope(), {stat_prefix_, name}, tags); + } + + Stats::Gauge& getGauge(const Stats::StatName& name, Stats::StatNameTagVector&& tags) { + return Stats::Utility::gaugeFromStatNames(context_.scope(), {stat_prefix_, name}, + Stats::Gauge::ImportMode::NeverImport, tags); + } + + Server::Configuration::ServerFactoryContext& context_; + Stats::StatNamePool stat_name_pool_; + const Stats::StatName stat_prefix_; + + const Stats::StatName disconnects_; + + const Stats::StatName status_code_; + const Stats::StatName disconnect_reason_; + const Stats::StatName cluster_label_; + const Stats::StatName cluster_value_; + + Stats::Counter& connection_attempts_counter_; + Stats::Counter& acks_received_counter_; + Stats::Counter& send_attempts_counter_; + Stats::Counter& sent_accepted_cnt_counter_; + Stats::Counter& sent_removed_cnt_counter_; + Stats::Counter& events_dropped_counter_; + Stats::Counter& queued_updates_counter_; + Stats::Counter& out_of_order_acks_counter_; + + Stats::Gauge& send_interval_gauge_; + Stats::Gauge& nonce_current_gauge_; + Stats::Gauge& nonce_acked_gauge_; + }; + + GrpcClientStats stats_; + + friend class GrpcClientTest; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.cc b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.cc new file mode 100644 index 0000000000000..440ac5ac68da9 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.cc @@ -0,0 +1,36 @@ +#include "contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.h" + +#include "envoy/registry/registry.h" + +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +ReverseTunnelReporterClientPtr +GrpcClientFactory::createClient(Server::Configuration::ServerFactoryContext& context, + const Protobuf::Message& config) { + const auto& grpc_client_config = + MessageUtil::downcastAndValidate( + config, context.messageValidationVisitor()); + return std::make_unique(context, grpc_client_config); +} + +std::string GrpcClientFactory::name() const { + return "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.clients.grpc_client"; +} + +ProtobufTypes::MessagePtr GrpcClientFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +REGISTER_FACTORY(GrpcClientFactory, ReverseTunnelReporterClientFactory); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.h b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.h new file mode 100644 index 0000000000000..765fc5a2d95a7 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.h @@ -0,0 +1,27 @@ +#pragma once + +#include "source/common/protobuf/utility.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/grpc_client.pb.h" +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/grpc_client.pb.validate.h" +#include "contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class GrpcClientFactory : public ReverseTunnelReporterClientFactory { +public: + ReverseTunnelReporterClientPtr createClient(Server::Configuration::ServerFactoryContext& context, + const Protobuf::Message& config) override; + + std::string name() const override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/reporters/BUILD b/contrib/reverse_tunnel_reporter/source/reporters/BUILD new file mode 100644 index 0000000000000..399bb153885c2 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/BUILD @@ -0,0 +1,16 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "reporters_lib", + deps = [ + "//contrib/reverse_tunnel_reporter/source/reporters/event_reporter:event_reporter_lib", + ], +) diff --git a/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/BUILD b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/BUILD new file mode 100644 index 0000000000000..ace33870375d9 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/BUILD @@ -0,0 +1,30 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "event_reporter_lib", + srcs = [ + "factory.cc", + "reporter.cc", + ], + hdrs = [ + "factory.h", + "reporter.h", + ], + deps = [ + "//contrib/reverse_tunnel_reporter/source:reverse_tunnel_event_types", + "//envoy/extensions/bootstrap/reverse_tunnel:reverse_tunnel_reporter_lib", + "//envoy/registry", + "//source/common/common:logger_lib", + "//source/common/config:utility_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters:pkg_cc_proto", + ], +) diff --git a/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.cc b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.cc new file mode 100644 index 0000000000000..d4f86fe296d64 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.cc @@ -0,0 +1,58 @@ +#include "contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.h" + +#include "envoy/registry/registry.h" + +#include "source/common/config/utility.h" +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +ReverseTunnelReporterPtr +EventReporterFactory::createReporter(Server::Configuration::ServerFactoryContext& context, + ProtobufTypes::MessagePtr config) { + const auto& reporter_config = MessageUtil::downcastAndValidate( + *config, context.messageValidationVisitor()); + + std::vector clients; + clients.reserve(reporter_config.clients().size()); + for (const auto& client_config : reporter_config.clients()) { + clients.push_back(createClient(context, client_config)); + } + return std::make_unique(context, reporter_config, std::move(clients)); +} + +std::string EventReporterFactory::name() const { + return "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.reporters.event_" + "reporter"; +} + +ProtobufTypes::MessagePtr EventReporterFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +ReverseTunnelReporterClientPtr +EventReporterFactory::createClient(Server::Configuration::ServerFactoryContext& context, + const ClientConfigProto& client_config) { + auto* factory = + Config::Utility::getFactoryByName(client_config.name()); + if (!factory) { + throw EnvoyException( + fmt::format("Unknown Reporter Client Factory: '{}'. " + "Make sure it is registered as a ReverseTunnelReporterClientFactory.", + client_config.name())); + } + + auto typed_config = Config::Utility::translateAnyToFactoryConfig( + client_config.typed_config(), context.messageValidationVisitor(), *factory); + return factory->createClient(context, *typed_config); +} + +REGISTER_FACTORY(EventReporterFactory, ReverseTunnelReporterFactory); + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.h b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.h new file mode 100644 index 0000000000000..14f272506c360 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.h @@ -0,0 +1,37 @@ +#pragma once + +#include "envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters/event_reporter.pb.h" +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters/event_reporter.pb.validate.h" +#include "contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h" +#include "contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +/// Factory that builds an EventReporter from its proto config, dynamically +/// resolving each child ReverseTunnelReporterClient by name. +class EventReporterFactory : public ReverseTunnelReporterFactory { +public: + ReverseTunnelReporterPtr createReporter(Server::Configuration::ServerFactoryContext& context, + ProtobufTypes::MessagePtr config) override; + std::string name() const override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + +private: + using ConfigProto = + envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig; + using ClientConfigProto = envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters:: + ReverseConnectionReporterClient; + + ReverseTunnelReporterClientPtr createClient(Server::Configuration::ServerFactoryContext& context, + const ClientConfigProto& client_config); +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.cc b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.cc new file mode 100644 index 0000000000000..e52a30d57c692 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.cc @@ -0,0 +1,140 @@ +#include "contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h" + +#include "source/common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +EventReporter::EventReporter(Server::Configuration::ServerFactoryContext& context, + const ConfigProto& config, + std::vector&& clients) + : context_{context}, clients_{std::move(clients)}, + stats_(generateStats( + PROTOBUF_GET_STRING_OR_DEFAULT(config, stat_prefix, "reverse_tunnel_reporter"), + context.scope())) { + ENVOY_LOG(info, "Constructed with {} clients", clients_.size()); +} + +void EventReporter::onServerInitialized() { + ENVOY_LOG(info, "Initialized"); + for (auto& client : clients_) { + client->onServerInitialized(this); + } +} + +void EventReporter::reportConnectionEvent(absl::string_view node_id, absl::string_view cluster_id, + absl::string_view tenant_id, int64_t initiation_time_ms) { + // Use the DP-side initiation timestamp when available; fall back to the injected time source + // for backward compatibility with older DP Envoys that don't send the header. + // + // initiation_time_ms is external, header-derived input. Converting an arbitrarily large + // millisecond count into SystemTime's finer-grained duration (nanoseconds on most platforms) + // would multiply by 1e6 and overflow int64 -> signed-overflow UB. Reject non-positive or + // out-of-range values and fall back to the time source instead. + constexpr int64_t max_representable_ms = + std::chrono::duration_cast(Envoy::SystemTime::duration::max()) + .count(); + const Envoy::SystemTime created_at = + (initiation_time_ms > 0 && initiation_time_ms <= max_representable_ms) + ? Envoy::SystemTime(std::chrono::duration_cast( + std::chrono::milliseconds(initiation_time_ms))) + : context_.timeSource().systemTime(); + auto ptr = std::make_shared(ReverseTunnelEvent::Connected{ + std::string(node_id), std::string(cluster_id), std::string(tenant_id), created_at}); + + context_.mainThreadDispatcher().post( + [this, ptr = std::move(ptr)]() mutable { this->addConnection(std::move(ptr)); }); +} + +void EventReporter::reportDisconnectionEvent(absl::string_view node_id, absl::string_view) { + std::string name = ReverseTunnelEvent::getName(node_id); + auto ptr = std::make_shared(name); + + context_.mainThreadDispatcher().post( + [this, ptr = std::move(ptr)]() mutable { this->removeConnection(std::move(ptr)); }); +} + +// This is only served on the main thread so no locks needed. +ReverseTunnelEvent::ConnectionsList EventReporter::getAllConnections() { + ASSERT(context_.mainThreadDispatcher().isThreadSafe()); + stats_.reverse_tunnel_full_pulls_total_.inc(); + + ReverseTunnelEvent::ConnectionsList all_connections; + all_connections.reserve(connections_.size()); + + for (auto& [key, val] : connections_) { + all_connections.push_back(val.connection); + } + return all_connections; +} + +EventReporterStats EventReporter::generateStats(const std::string& prefix, Stats::Scope& scope) { + return EventReporterStats{ALL_EVENT_REPORTER_STATS(POOL_COUNTER_PREFIX(scope, prefix), + POOL_GAUGE_PREFIX(scope, prefix))}; +} + +void EventReporter::notifyClients(ReverseTunnelEvent::TunnelUpdates&& updates) { + ASSERT(!clients_.empty(), "Need at least one client. Enforced via the protos."); + + for (size_t i = 0; i < clients_.size() - 1; i++) { + clients_[i]->receiveEvents(updates); + } + + clients_.back()->receiveEvents(std::move(updates)); +} + +void EventReporter::addConnection(std::shared_ptr&& connection) { + ASSERT(context_.mainThreadDispatcher().isThreadSafe()); + + ENVOY_LOG(info, "Accepted a new connection. Node: {}, Cluster: {}, Tenant: {}", + connection->node_id, connection->cluster_id, connection->tenant_id); + + std::string name = ReverseTunnelEvent::getName(connection->node_id); + auto [it, inserted] = connections_.try_emplace(std::move(name), std::move(connection), 1); + + if (inserted) { + stats_.reverse_tunnel_unique_active_.inc(); + notifyClients(ReverseTunnelEvent::TunnelUpdates{{it->second.connection}, {}}); + } else { + // Multiple reverse tunnels can share the same name (same node). + // We ref-count them and only notify clients of removal when the last one disconnects. + it->second.count++; + } + + stats_.reverse_tunnel_established_total_.inc(); + stats_.reverse_tunnel_active_.inc(); +} + +void EventReporter::removeConnection( + std::shared_ptr&& disconnection) { + ASSERT(context_.mainThreadDispatcher().isThreadSafe()); + + const auto& name = disconnection->name; + auto it = connections_.find(name); + + ENVOY_LOG(info, "Removed connection. Name: {}", name); + + if (it == connections_.end()) { + ENVOY_LOG(warn, "Tried to remove a connection which doesnt exist"); + return; + } + + // Only notify removal on the last ref — see addConnection for the ref-count rationale. + if (it->second.count == 1) { + connections_.erase(it); + stats_.reverse_tunnel_unique_active_.dec(); + notifyClients(ReverseTunnelEvent::TunnelUpdates{{}, {disconnection}}); + } else { + it->second.count--; + } + + stats_.reverse_tunnel_closed_total_.inc(); + stats_.reverse_tunnel_active_.dec(); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h new file mode 100644 index 0000000000000..898ac2fe2598a --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include "envoy/stats/stats_macros.h" + +#include "source/common/common/logger.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters/event_reporter.pb.h" +#include "contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +#define ALL_EVENT_REPORTER_STATS(COUNTER, GAUGE) \ + COUNTER(reverse_tunnel_established_total) \ + COUNTER(reverse_tunnel_closed_total) \ + COUNTER(reverse_tunnel_full_pulls_total) \ + GAUGE(reverse_tunnel_active, Accumulate) \ + GAUGE(reverse_tunnel_unique_active, Accumulate) + +struct EventReporterStats { + ALL_EVENT_REPORTER_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +struct ConnectionEntry { + std::shared_ptr connection; + std::size_t count; +}; + +/// Aggregates reverse-tunnel connection/disconnection events, de-duplicates by +/// name, maintains stats, and fans out batched diffs as shared ptrs to registered clients. +class EventReporter : public ReverseTunnelReporterWithState, + public Logger::Loggable { +public: + using ConfigProto = + envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig; + + EventReporter(Server::Configuration::ServerFactoryContext& context, const ConfigProto& config, + std::vector&& clients); + + void onServerInitialized() override; + void reportConnectionEvent(absl::string_view node_id, absl::string_view cluster_id, + absl::string_view tenant_id, int64_t initiation_time_ms) override; + void reportDisconnectionEvent(absl::string_view node_id, absl::string_view cluster_id) override; + ReverseTunnelEvent::ConnectionsList getAllConnections() override; + +private: + static EventReporterStats generateStats(const std::string& prefix, Stats::Scope& scope); + void notifyClients(ReverseTunnelEvent::TunnelUpdates&& updates); + void addConnection(std::shared_ptr&& connection); + void removeConnection(std::shared_ptr&& disconnection); + + Server::Configuration::ServerFactoryContext& context_; + std::vector clients_; + EventReporterStats stats_; + // Keyed by getName(node_id). + absl::flat_hash_map connections_; +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h b/contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h new file mode 100644 index 0000000000000..5831bdd636492 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/common/time.h" +#include "envoy/config/typed_config.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h" +#include "envoy/server/factory_context.h" + +#include "source/common/common/fmt.h" + +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +// The namespace holding the structs for the reverse tunnel events +namespace ReverseTunnelEvent { +// getName builds the canonical connection name used as the de-duplication key in the reporter. +// Assumption: node_id is unique. +// When tenant isolation is enabled we get the node_id here as +// ReverseConnectionUtility::buildTenantScopedIdentifier(node_id) from the reverse_tunnel code which +// invokes this. +inline std::string getName(absl::string_view node_id) { return std::string(node_id); } + +struct Connected { + std::string node_id; + std::string cluster_id; + std::string tenant_id; + Envoy::SystemTime created_at; +}; + +struct Disconnected { + std::string name; + + explicit Disconnected(absl::string_view n) : name(n) {} +}; + +// Most of the events are just single connections and disconnections. +// The clients may need to buffer them up -> some extra stack space for the same. +using ConnectionsList = absl::InlinedVector, 4>; +using DisconnectionsList = absl::InlinedVector, 4>; + +struct TunnelUpdates { + ConnectionsList connections; + DisconnectionsList disconnections; + + std::size_t size() const { return connections.size() + disconnections.size(); } + + void operator+=(TunnelUpdates&& events) { + connections.insert(connections.end(), std::make_move_iterator(events.connections.begin()), + std::make_move_iterator(events.connections.end())); + + disconnections.insert(disconnections.end(), + std::make_move_iterator(events.disconnections.begin()), + std::make_move_iterator(events.disconnections.end())); + + events.connections.clear(); + events.disconnections.clear(); + } + + void clear() { + connections.clear(); + disconnections.clear(); + } +}; +} // namespace ReverseTunnelEvent + +// ReverseTunnelReporterWithState will own the clients and expose an api for them to get the full +// state. ReverseTunnelReporterWithState allows multiple clients to share data -> clients can focus +// on sending the data alone. +class ReverseTunnelReporterWithState : public ReverseTunnelReporter { +public: + ~ReverseTunnelReporterWithState() override = default; + + virtual ReverseTunnelEvent::ConnectionsList getAllConnections() PURE; +}; + +using ReverseTunnelReporterWithStatePtr = std::unique_ptr; + +// ReverseTunnelReporterClient gets the ptr to the reporter for polling all the active connections. +// ReverseTunnelReporterClient also has receiveEvents to get the diff events from the reporter. +class ReverseTunnelReporterClient { +public: + virtual ~ReverseTunnelReporterClient() = default; + + virtual void onServerInitialized(ReverseTunnelReporterWithState* reporter) PURE; + + virtual void receiveEvents(ReverseTunnelEvent::TunnelUpdates events) PURE; +}; + +using ReverseTunnelReporterClientPtr = std::unique_ptr; + +class ReverseTunnelReporterClientFactory : public Config::TypedFactory { +public: + virtual ReverseTunnelReporterClientPtr + createClient(Server::Configuration::ServerFactoryContext& context, + const Protobuf::Message& config) PURE; + + std::string category() const override { + return "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.clients"; + } +}; + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/test/clients/BUILD b/contrib/reverse_tunnel_reporter/test/clients/BUILD new file mode 100644 index 0000000000000..74fff54daa5ee --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/clients/BUILD @@ -0,0 +1,71 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_cc_test_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "grpc_client_test", + srcs = ["grpc_client_test.cc"], + deps = [ + "//contrib/reverse_tunnel_reporter/source/clients/grpc_client:grpc_client_lib", + "//envoy/grpc:async_client_interface", + "//envoy/registry", + "//source/common/api:api_lib", + "//source/common/protobuf:utility_lib_header", + "//test/mocks/grpc:grpc_mocks", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/server:server_factory_context_mocks", + "//test/mocks/upstream:cluster_info_mocks", + "//test/mocks/upstream:thread_local_cluster_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_cc_test_library( + name = "integration_test_utils_lib", + hdrs = ["integration_test_utils.h"], + deps = [ + "//source/common/common:fmt_lib", + "//test/config:utility_lib", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters:pkg_cc_proto", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/router/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/reverse_tunnel/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "grpc_client_integration_test", + srcs = ["grpc_client_integration_test.cc"], + deps = [ + ":integration_test_utils_lib", + "//contrib/reverse_tunnel_reporter/source:config", + "//source/common/thread_local:thread_local_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_connection_resolver_lib", + "//source/extensions/bootstrap/reverse_tunnel/downstream_socket_interface:reverse_tunnel_initiator_lib", + "//source/extensions/bootstrap/reverse_tunnel/upstream_socket_interface:reverse_tunnel_acceptor_lib", + "//source/extensions/filters/http/router:config", + "//source/extensions/filters/network/http_connection_manager:config", + "//source/extensions/filters/network/reverse_tunnel:config", + "//test/integration:integration_lib", + "@com_github_grpc_grpc//:grpc++", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client:pkg_cc_grpc", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client:pkg_cc_proto", + "@envoy_api//contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters:pkg_cc_proto", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/upstreams/http/v3:pkg_cc_proto", + ], +) diff --git a/contrib/reverse_tunnel_reporter/test/clients/grpc_client_integration_test.cc b/contrib/reverse_tunnel_reporter/test/clients/grpc_client_integration_test.cc new file mode 100644 index 0000000000000..eec30810a010d --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/clients/grpc_client_integration_test.cc @@ -0,0 +1,482 @@ +#include +#include +#include +#include +#include + +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/extensions/upstreams/http/v3/http_protocol_options.pb.h" + +#include "test/integration/integration.h" +#include "test/integration/utility.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/grpc_client.pb.h" +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/stream_reverse_tunnels.grpc.pb.h" +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters/event_reporter.pb.h" +#include "contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h" +#include "contrib/reverse_tunnel_reporter/test/clients/integration_test_utils.h" +#include "grpc++/grpc++.h" +#include "grpc++/server.h" +#include "grpc++/server_builder.h" +#include "grpc++/server_context.h" +#include "gtest/gtest.h" +#include "integration_test_utils.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +constexpr absl::string_view reporterName = + "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.reporters.event_reporter"; +constexpr absl::string_view grpcClient = + "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.clients.grpc_client"; +constexpr std::chrono::milliseconds serverWait{5}; +constexpr std::size_t sendInterval{500}; +constexpr std::size_t maxRetries{3}; + +using envoy::extensions::reverse_tunnel_reporters::v3alpha::clients::grpc_client::GrpcClientConfig; +using envoy::extensions::reverse_tunnel_reporters::v3alpha::clients::grpc_client:: + ReverseTunnelReportingService; +using envoy::extensions::reverse_tunnel_reporters::v3alpha::clients::grpc_client:: + StreamReverseTunnelsRequest; +using envoy::extensions::reverse_tunnel_reporters::v3alpha::clients::grpc_client:: + StreamReverseTunnelsResponse; +using envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig; +using envoy::extensions::upstreams::http::v3::HttpProtocolOptions; + +class TestingService final : public ReverseTunnelReportingService::Service { + absl::flat_hash_map state_; + std::atomic num_events_; + std::optional newInterval; + +public: + grpc::Status StreamReverseTunnels( + grpc::ServerContext* /*context*/, + grpc::ServerReaderWriter* stream) + override { + StreamReverseTunnelsRequest request; + int cnt{0}; + + ENVOY_LOG_MISC(error, "GrpcClientIntegrationTest: Status=Connected"); + + while (stream->Read(&request)) { + cnt++; + StreamReverseTunnelsResponse response; + response.set_request_nonce(request.nonce()); + + if (newInterval.has_value()) { + *response.mutable_report_interval() = + Protobuf::util::TimeUtil::MillisecondsToDuration(newInterval.value().count()); + } + + if (!stream->Write(response)) { + ENVOY_LOG_MISC(error, "GrpcClientIntegrationTest: Unable to send the response: {}", + response.DebugString()); + break; + } + + process(request); + } + + ENVOY_LOG_MISC(error, "GrpcClientIntegrationTest: Stream ended, total messages: {}", cnt); + + return grpc::Status::OK; + } + + void process(StreamReverseTunnelsRequest& req) { + for (auto& conn : req.added_tunnels()) { + state_[conn.name()] = + ReverseTunnelEvent::Connected{.node_id = conn.identity().node_id(), + .cluster_id = conn.identity().cluster_id(), + .tenant_id = conn.identity().tenant_id(), + .created_at = Envoy::SystemTime{}}; + } + + for (auto& name : req.removed_tunnel_names()) { + state_.erase(name); + } + + num_events_ += 1; + } + + absl::flat_hash_map getState() { return state_; } + + std::size_t numEvents() { return num_events_.load(); } + + void setInterval(std::chrono::milliseconds ms) { newInterval = ms; } +}; + +struct GrpcServer { + std::unique_ptr server_; + std::thread server_thread_; + TestingService service_; + + explicit GrpcServer(absl::string_view localhost) { + std::string server_address = fmt::format("{}:{}", localhost, reportingPort); + grpc::ServerBuilder builder; + + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + builder.RegisterService(&service_); + + server_ = std::unique_ptr(builder.BuildAndStart()); + server_thread_ = std::thread([this] { server_->Wait(); }); + } + + ~GrpcServer() { + auto deadline = std::chrono::system_clock::now() + serverWait; // NO_CHECK_FORMAT(real_time) + server_->Shutdown(deadline); + server_thread_.join(); + } +}; + +class GrpcClientIntegrationTest : public testing::TestWithParam, + public BaseIntegrationTest { +public: + GrpcClientIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::baseConfigNoListeners()) {} + + void initialize() override { + use_lds_ = true; + + std::string localhost = Network::Test::getLoopbackAddressString(GetParam()); + std::string anyhost = Network::Test::getAnyAddressString(GetParam()); + + addBootstrapExtension(getUpstreamExtension(getReporterConfig(), enable_tenant_isolation_), + config_helper_); + addBootstrapExtension(getDownstreamExtension(), config_helper_); + addCluster(getDownstreamCluster(localhost), config_helper_); + addListener(getUpstreamListener(anyhost), config_helper_, current_listeners_); + + auto cluster = getUpstreamCluster(localhost); + addCluster(getHttp2Cluster(cluster), config_helper_); + + BaseIntegrationTest::initialize(); + + current_config_ = ConfigHelper{version_, config_helper_.bootstrap()}; + } + +protected: + EventReporterConfig getReporterConfig() { + EventReporterConfig cfg; + cfg.set_stat_prefix(reporterName); + + auto* client = cfg.add_clients(); + client->set_name(grpcClient); + + GrpcClientConfig grpc_cfg; + grpc_cfg.set_stat_prefix("reverse_connection_grpc_client"); + grpc_cfg.set_cluster(upstreamCluster); + *(grpc_cfg.mutable_default_send_interval()) = + Protobuf::util::TimeUtil::MillisecondsToDuration(sendInterval); + *(grpc_cfg.mutable_connect_retry_interval()) = + Protobuf::util::TimeUtil::MillisecondsToDuration(sendInterval); + grpc_cfg.set_max_retries(maxRetries); + grpc_cfg.set_max_buffer_count(1'000'000); + std::ignore = client->mutable_typed_config()->PackFrom(grpc_cfg); + + return cfg; + } + + void updateLds(ConfigHelper& new_config) { + new_config.setLds(std::to_string(++cur_version_)); + // Wait for up to a minute for the values to propagate. + test_server_->waitForGauge("listener_manager.total_listeners_active", + testing::Eq(current_listeners_), std::chrono::seconds(60)); + current_config_ = std::move(new_config); + } + + void addListenerLds(Listener&& listener) { + ConfigHelper new_config{version_, current_config_.bootstrap()}; + addListener(std::move(listener), new_config, current_listeners_); + updateLds(new_config); + } + + void addListenerLds(std::vector&& listeners) { + ConfigHelper new_config{version_, current_config_.bootstrap()}; + + for (auto& listener : listeners) { + addListener(std::move(listener), new_config, current_listeners_); + } + + updateLds(new_config); + } + + void removeListenerLds(const std::string& name) { + ConfigHelper new_config{version_, current_config_.bootstrap()}; + removeListener(name, new_config, current_listeners_); + updateLds(new_config); + } + + void removeListenerLds(std::vector& names) { + ConfigHelper new_config{version_, current_config_.bootstrap()}; + + for (auto& name : names) { + removeListener(name, new_config, current_listeners_); + } + + updateLds(new_config); + } + + void + validateEqual(std::chrono::milliseconds ms, + const absl::flat_hash_map& expected) { + timeSystem().advanceTimeWait(ms); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // This is fine no concern about thread safety -> we have slept for some time and no extra + // events should be coming now. + auto state = grpc_server_->service_.getState(); + // EXPECT_THAT(state_, testing::ContainerEq(expected)); + EXPECT_EQ(state.size(), expected.size()); + } + + absl::flat_hash_map + getConns(std::vector node_ids) { + absl::flat_hash_map connections; + + for (auto& node_id : node_ids) { + connections[ReverseTunnelEvent::getName(node_id)] = + ReverseTunnelEvent::Connected{.node_id = node_id, + .cluster_id = std::string(downstreamCluster), + .tenant_id = std::string(downstreamTenant), + .created_at = Envoy::SystemTime{}}; + } + + return connections; + } + + void makeNewServer() { + grpc_server_ = + std::make_unique(Network::Test::getLoopbackAddressUrlString(GetParam())); + } + + std::string getTenantIsolatedName(std::string name) { + return fmt::format("{}:{}", downstreamTenant, name); + } + + std::vector getTenantIsolatedNames(std::vector names) { + for (auto& name : names) { + name = getTenantIsolatedName(name); + } + + return names; + } + + void setNewSendInterval(std::chrono::milliseconds ms) { grpc_server_->service_.setInterval(ms); } + + std::size_t numEvents() { return grpc_server_->service_.numEvents(); } + + std::function callback_; + std::unique_ptr grpc_server_; + + int current_listeners_{0}; + int cur_version_{0}; + + ConfigHelper current_config_{version_, config_helper_.bootstrap()}; + bool enable_tenant_isolation_{false}; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, GrpcClientIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +/** + * Test: HappyPath + * Tests the standard lifecycle: gRPC stream establishment, reporting new + * tunnel additions via LDS, and reporting removals when listeners are deleted. + */ +TEST_P(GrpcClientIntegrationTest, HappyPath) { + makeNewServer(); + initialize(); + + addListenerLds(getDownstreamListener("node-1", 1)); + addListenerLds(getDownstreamListener("node-2", 1)); + + // Give envoy time to make the rc, connect to the test server, timer to fire + // and send the updates. + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({"node-1", "node-2"})); + + removeListenerLds("node-1"); + removeListenerLds("node-2"); + + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({})); +} + +/** + * Test: TenantIsolation. + * Tests the working of the code when enabled with tenant isolation. + */ +TEST_P(GrpcClientIntegrationTest, TenantIsolation) { + enable_tenant_isolation_ = true; + + initialize(); + makeNewServer(); + + addListenerLds(getDownstreamListener("node-1", 1)); + addListenerLds(getDownstreamListener("node-2", 1)); + validateEqual(std::chrono::milliseconds(sendInterval * 3), + getConns(getTenantIsolatedNames({"node-1", "node-2"}))); + + removeListenerLds("node-1"); + removeListenerLds("node-2"); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({})); +} + +/** + * Test: ServerReconnect + * Tests that Envoy performs a "Full State Push" upon reconnection. + * Even if tunnels didn't change, they must be re-reported on a new gRPC stream. + */ +TEST_P(GrpcClientIntegrationTest, ServerReconnect) { + makeNewServer(); + initialize(); + + addListenerLds(getDownstreamListener("node-1", 1)); + addListenerLds(getDownstreamListener("node-2", 1)); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({"node-1", "node-2"})); + + makeNewServer(); + // Time to disconnect, connect and then full push. + validateEqual(std::chrono::milliseconds(sendInterval * 5), getConns({"node-1", "node-2"})); + + removeListenerLds("node-1"); + removeListenerLds("node-2"); + + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({})); +} + +/** + * Test: EventsWhenServerIsDead + * Tests "Event Convergence." If state changes (Add/Remove) happen while the + * server is down, the client must report the final ground truth once back online. + */ +TEST_P(GrpcClientIntegrationTest, EventsWhenServerIsDead) { + makeNewServer(); + initialize(); + + addListenerLds(getDownstreamListener("node-1", 1)); + addListenerLds(getDownstreamListener("node-2", 1)); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({"node-1", "node-2"})); + + grpc_server_ = nullptr; + addListenerLds(getDownstreamListener("node-3", 1)); + test_server_->waitForGauge("listener.upstreamListener.downstream_cx_active", testing::Eq(3), + std::chrono::milliseconds(sendInterval * 3)); + removeListenerLds("node-1"); + // Wait for the connections to establish and drain + test_server_->waitForGauge("listener.upstreamListener.downstream_cx_active", testing::Eq(2), + std::chrono::milliseconds(sendInterval * 3)); + + makeNewServer(); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({"node-3", "node-2"})); + + removeListenerLds("node-2"); + removeListenerLds("node-3"); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({})); +} + +/** + * Test: ServerDead + * Tests the gRPC client's retry engine. It verifies that the client + * continues to attempt connections based on 'connect_retry_interval' forever. + * We have limited to max_retries + 2 as we had to stop somewhere. + */ +TEST_P(GrpcClientIntegrationTest, ServerDead) { + initialize(); + // First one on server init. + test_server_->waitForCounter( + "reverse_connection_grpc_client.connection_attempts.cluster.upstreamCluster", testing::Eq(1)); + + for (std::size_t i = 0; i <= maxRetries; i++) { + // Wait for the timer to fire first. + timeSystem().advanceTimeWait(std::chrono::milliseconds(sendInterval)); + test_server_->waitForCounter( + "reverse_connection_grpc_client.connection_attempts.cluster.upstreamCluster", + testing::Eq(i + 2)); + } +} + +/** + * Test: ServerLate + * Tests the "Catch-up" scenario. Verifies that if tunnels exist before the + * reporting service is reachable, the client pushes the state immediately on connection. + */ +TEST_P(GrpcClientIntegrationTest, ServerLate) { + initialize(); + + addListenerLds(getDownstreamListener("node-1", 1)); + addListenerLds(getDownstreamListener("node-2", 1)); + // Wait for the connections to establish. + test_server_->waitForGauge("listener.upstreamListener.downstream_cx_active", testing::Eq(2), + std::chrono::milliseconds(sendInterval * 3)); + + makeNewServer(); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({"node-1", "node-2"})); + + removeListenerLds("node-1"); + removeListenerLds("node-2"); + validateEqual(std::chrono::milliseconds(sendInterval * 3), getConns({})); +} + +/** + * Test: ServerSentTime. + * Tests that the client uses the time sent by the server for the updates. + * Overriding the default value. + */ +TEST_P(GrpcClientIntegrationTest, ServerSentTime) { + initialize(); + makeNewServer(); + + std::size_t num_events_1 = numEvents(); + timeSystem().advanceTimeWait(std::chrono::milliseconds(2 * sendInterval)); + std::size_t num_events_2 = numEvents(); + + EXPECT_GE(num_events_2 - num_events_1, 1); + + // Now change the send Interval to sendInterval / 5. + setNewSendInterval(std::chrono::milliseconds(sendInterval / 5)); + timeSystem().advanceTimeWait(std::chrono::milliseconds(2 * sendInterval)); + std::size_t num_events_3 = numEvents(); + + // This would have been scheduled and so we dont expect a change in the scheduling. + EXPECT_GE(num_events_3 - num_events_2, 1); + + // Now we track the numEvents and check that the sendInterval is smaller -> more events. + timeSystem().advanceTimeWait(std::chrono::milliseconds(2 * sendInterval)); + std::size_t num_events_4 = numEvents(); + EXPECT_GE(num_events_4 - num_events_3, 5); +} + +// Only run the load test in optimized builds. +#if defined(NDEBUG) +TEST_P(GrpcClientIntegrationTest, LoadTest) { + makeNewServer(); + initialize(); + + // Limited to 1000 to reduce test flakiness. + int sz = 1000; + std::vector nodes(sz); + for (int i = 0; i < sz; i++) { + nodes[i] = fmt::format("node-{}", i); + } + + std::vector listeners(sz); + for (int i = 0; i < sz; i++) { + listeners[i] = getDownstreamListener(nodes[i], 1); + } + + addListenerLds(std::move(listeners)); + // Pure overhead of running the client should be minimal. + validateEqual(std::chrono::milliseconds(sendInterval * 5), getConns(nodes)); + + removeListenerLds(nodes); + // Allow more time for the discovery of listener removal and then propogation. + validateEqual(std::chrono::milliseconds(sendInterval * 10), getConns({})); +} +#endif // defined(NDEBUG) + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/test/clients/grpc_client_test.cc b/contrib/reverse_tunnel_reporter/test/clients/grpc_client_test.cc new file mode 100644 index 0000000000000..5c0476609f3e4 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/clients/grpc_client_test.cc @@ -0,0 +1,812 @@ +#include + +#include "envoy/grpc/async_client.h" +#include "envoy/registry/registry.h" + +#include "source/common/grpc/common.h" +#include "source/common/protobuf/utility.h" + +#include "test/mocks/grpc/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/server/server_factory_context.h" +#include "test/mocks/upstream/cluster_info.h" +#include "test/mocks/upstream/thread_local_cluster.h" +#include "test/test_common/utility.h" + +#include "contrib/reverse_tunnel_reporter/source/clients/grpc_client/client.h" +#include "contrib/reverse_tunnel_reporter/source/clients/grpc_client/factory.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class MockReverseTunnelReporter : public ReverseTunnelReporterWithState { +public: + MOCK_METHOD(void, onServerInitialized, (), (override)); + MOCK_METHOD(void, reportConnectionEvent, + (absl::string_view, absl::string_view, absl::string_view, int64_t), (override)); + MOCK_METHOD(void, reportDisconnectionEvent, (absl::string_view, absl::string_view), (override)); + MOCK_METHOD(ReverseTunnelEvent::ConnectionsList, getAllConnections, (), (override)); +}; + +GrpcConfigProto mockConfig() { + GrpcConfigProto config_; + + config_.set_cluster("test_cluster"); + config_.mutable_default_send_interval()->set_seconds(2); + config_.mutable_connect_retry_interval()->set_seconds(2); + config_.set_max_retries(2); + config_.set_max_buffer_count(1000); + config_.set_stat_prefix("test.grpc_client"); + + return config_; +} + +ReverseTunnelEvent::ConnectionsList makeConnections(std::vector node_ids) { + ReverseTunnelEvent::ConnectionsList connections; + std::string cluster = "test_cluster"; + std::string tenant = "test_tenant"; + + for (const auto& node : node_ids) { + auto conn = std::make_shared(ReverseTunnelEvent::Connected{ + node, cluster, tenant, std::chrono::system_clock::time_point(std::chrono::seconds(1))}); + connections.push_back(std::move(conn)); + } + + return connections; +} + +ReverseTunnelEvent::DisconnectionsList makeDisconnections(std::vector node_ids) { + std::string cluster = "test_cluster"; + ReverseTunnelEvent::DisconnectionsList disconnections; + + for (const auto& node : node_ids) { + auto disconn = std::make_shared( + ReverseTunnelEvent::Disconnected{ReverseTunnelEvent::getName(node)}); + disconnections.push_back(std::move(disconn)); + } + + return disconnections; +} + +StreamTunnelsResp validateReq(Buffer::InstancePtr& request, + const ReverseTunnelEvent::TunnelUpdates& actual, bool full_push) { + StreamTunnelsReq req; + bool success = Grpc::Common::parseBufferInstance(std::move(request), req); + EXPECT_EQ(success, true); + + EXPECT_EQ(req.added_tunnels_size(), actual.connections.size()); + EXPECT_EQ(req.removed_tunnel_names_size(), actual.disconnections.size()); + + for (std::size_t i = 0; i < actual.connections.size(); i++) { + EXPECT_EQ(actual.connections[i]->node_id, req.added_tunnels(i).identity().node_id()); + } + + for (std::size_t i = 0; i < actual.disconnections.size(); i++) { + EXPECT_EQ(actual.disconnections[i]->name, req.removed_tunnel_names(i)); + } + + EXPECT_EQ(full_push, req.full_push()); + + StreamTunnelsResp resp; + resp.set_request_nonce(req.nonce()); + + return resp; +} + +Protobuf::Duration getHalfDuration(const Protobuf::Duration& dur) { + return Protobuf::util::TimeUtil::MillisecondsToDuration( + DurationUtil::durationToMilliseconds(dur) / 2); +} + +class GrpcClientTest : public testing::Test { +public: + GrpcClientTest() = default; + + void SetUp() override { + api_ = Api::createApiForTest(time_system_); + dispatcher_ = api_->allocateDispatcher("test_thread"); + + ON_CALL(context_, mainThreadDispatcher()).WillByDefault(ReturnRef(*dispatcher_)); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(*stats_store_.rootScope())); + ON_CALL(context_, clusterManager()).WillByDefault(ReturnRef(cm_)); + + ON_CALL(context_, localInfo()).WillByDefault(ReturnRef(local_info_)); + ON_CALL(local_info_, nodeName()).WillByDefault(ReturnRef(node_id)); + ON_CALL(local_info_, clusterName()).WillByDefault(ReturnRef(cluster_id)); + + ON_CALL(cm_, getThreadLocalCluster(_)).WillByDefault(Return(&thread_local_cluster_)); + ON_CALL(thread_local_cluster_, info()).WillByDefault(Return(cluster_info_)); + ON_CALL(*cluster_info_, statsScope()).WillByDefault(ReturnRef(*stats_store_.rootScope())); + + ON_CALL(cm_, grpcAsyncClientManager()).WillByDefault(ReturnRef(manager_)); + ON_CALL(manager_, getOrCreateRawAsyncClient(_, _, _)) + .WillByDefault(Return(absl::StatusOr(async_client_))); + } + +protected: + void incTime(const Protobuf::Duration& dur) { + time_system_.advanceTimeAsyncImpl( + std::chrono::milliseconds(DurationUtil::durationToMilliseconds(dur))); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + } + + void getStream(int times) { + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)) + .Times(times) + .WillRepeatedly(Invoke([this](absl::string_view, absl::string_view, + Grpc::RawAsyncStreamCallbacks& callbacks, + const Http::AsyncClient::StreamOptions&) { + callbacks_ = &callbacks; + return async_stream_.get(); + })); + } + + GrpcClient::GrpcClientStats getStats() { + return GrpcClient::GrpcClientStats{context_, config_.stat_prefix(), config_.cluster()}; + } + + std::shared_ptr> async_client_{ + std::make_shared>()}; + std::unique_ptr> async_stream_{ + std::make_unique>()}; + Grpc::RawAsyncStreamCallbacks* callbacks_; + + NiceMock context_; + NiceMock cm_; + NiceMock thread_local_cluster_; + NiceMock local_info_; + NiceMock manager_; + std::shared_ptr> cluster_info_{ + std::make_shared>()}; + + Api::ApiPtr api_; + Event::SimulatedTimeSystem time_system_; + Event::DispatcherPtr dispatcher_; + Stats::IsolatedStoreImpl stats_store_; + + std::string node_id{"tunnel-v2"}; + std::string cluster_id{"tunnel-v2"}; + + GrpcConfigProto config_{mockConfig()}; + NiceMock mock_reporter_; +}; + +// Check the connection behaviour on server initialization (infinite retries) +TEST_F(GrpcClientTest, RetryAttemptsOnStreamCreationFailure) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + // The connection attempts shld not be bound by anything. + // Making it config_.max_retries + 2 for a simple check of not bound by max_retries. + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)) + .Times(config_.max_retries() + 2) + .WillRepeatedly(Return(nullptr)); + + client.onServerInitialized(&mock_reporter_); + + for (std::size_t i = 0; i < config_.max_retries() + 1; i++) { + incTime(config_.connect_retry_interval()); + } + + // Not incremented because no connection attempt was successful. + // startRaw => nullptr. + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 0); + EXPECT_EQ(stats_.send_attempts_counter_.value(), 0); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), 0); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), 0); + EXPECT_EQ(stats_ + .getCounter(stats_.disconnects_, + stats_.getTags( + Grpc::Status::WellKnownGrpcStatus::Internal, + GrpcDisconnectionReason::DisconnectReason::STREAM_CREATION_FAILED)) + .value(), + config_.max_retries() + 2); +} + +// Checks the happy path -> server connect and full push. +TEST_F(GrpcClientTest, ClientSendsFullPushOnConnect) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + ReverseTunnelEvent::TunnelUpdates events{makeConnections({"node_1"}), {}}; + + EXPECT_CALL(mock_reporter_, getAllConnections()).WillOnce(Return(events.connections)); + + getStream(1); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .WillOnce(Invoke([&events](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, true); + EXPECT_EQ(resp.request_nonce(), 1); + })); + + client.onServerInitialized(&mock_reporter_); + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.acks_received_counter_.value(), 0); + EXPECT_EQ(stats_.send_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.events_dropped_counter_.value(), 0); + EXPECT_EQ(stats_.queued_updates_counter_.value(), 1); + EXPECT_EQ(stats_.out_of_order_acks_counter_.value(), 0); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), 1); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), 0); + EXPECT_EQ(stats_.send_interval_gauge_.value(), + DurationUtil::durationToMilliseconds(config_.default_send_interval())); + EXPECT_EQ(stats_.sent_accepted_cnt_counter_.value(), 1); + EXPECT_EQ(stats_.sent_removed_cnt_counter_.value(), 0); +} + +// Checks the happy path -> Server up connect and send the diff after the full push +TEST_F(GrpcClientTest, ClientSendsDiffAfterFullPush) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + int cur = 0, total = 4; + + ReverseTunnelEvent::TunnelUpdates updates[] = { + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_1"}), {}}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_2"}), + makeDisconnections({"node_1"})}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_3", "node_4"}), {}}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_5"}), + makeDisconnections({"node_3"})}}; + + getStream(1); + + EXPECT_CALL(mock_reporter_, getAllConnections()).WillOnce(Invoke([&updates]() { + return updates[0].connections; + })); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(total) + .WillRepeatedly(Invoke([this, &updates, &cur](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, updates[cur], cur == 0); + EXPECT_EQ(resp.request_nonce(), cur + 1); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + client.onServerInitialized(&mock_reporter_); + cur++; + + for (; cur < total; cur++) { + client.receiveEvents(updates[cur]); + incTime(config_.default_send_interval()); + } + + int total_events = 0; + for (int i = 0; i < total; i++) { + total_events += updates[i].size(); + } + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.acks_received_counter_.value(), total); + EXPECT_EQ(stats_.send_attempts_counter_.value(), total); + EXPECT_EQ(stats_.sent_accepted_cnt_counter_.value(), 5); + EXPECT_EQ(stats_.sent_removed_cnt_counter_.value(), 2); + EXPECT_EQ(stats_.events_dropped_counter_.value(), 0); + EXPECT_EQ(stats_.queued_updates_counter_.value(), total_events); + EXPECT_EQ(stats_.out_of_order_acks_counter_.value(), 0); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), total); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), total); + EXPECT_EQ(stats_.send_interval_gauge_.value(), + DurationUtil::durationToMilliseconds(config_.default_send_interval())); +} + +// Check the happy path -> config changes from the server response is applied +TEST_F(GrpcClientTest, ReportIntervalChangesReflectInClient) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + int cur = 0; + + ReverseTunnelEvent::TunnelUpdates events; + + getStream(1); + + EXPECT_CALL(mock_reporter_, getAllConnections()).WillOnce(Invoke([&events]() { + return events.connections; + })); + + // This should be called 3 times. + // Once for the first message and then twice for the next two increments. + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(3) + .WillRepeatedly(Invoke([this, &events, &cur](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, cur == 0); + EXPECT_EQ(resp.request_nonce(), ++cur); + + if (cur == 1) { + *resp.mutable_report_interval() = getHalfDuration(config_.default_send_interval()); + } + + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + EXPECT_EQ(stats_.send_interval_gauge_.value(), + DurationUtil::durationToMilliseconds(config_.default_send_interval())); + client.onServerInitialized(&mock_reporter_); + EXPECT_EQ(stats_.send_interval_gauge_.value(), + DurationUtil::durationToMilliseconds(getHalfDuration(config_.default_send_interval()))); + + // This is already scheduled from the next time we will use the half interval for sending. + incTime(config_.default_send_interval()); + incTime(getHalfDuration(config_.default_send_interval())); + + EXPECT_EQ(stats_.send_attempts_counter_.value(), 3); +} + +// Check edge case -> Full push and then diffs on reconnect +TEST_F(GrpcClientTest, FullPushAndDiffOnReconnect) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + int cur = 0, total = 4; + + ReverseTunnelEvent::TunnelUpdates updates[] = { + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_1"}), {}}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_2"}), + makeDisconnections({"node_1"})}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_3", "node_4"}), {}}, + ReverseTunnelEvent::TunnelUpdates{makeConnections({"node_5"}), + makeDisconnections({"node_3"})}}; + + // 2 stream creations: initial connect + reconnect after remote close. + getStream(2); + + // getAllConnections is called once per full push (initial + reconnect). + EXPECT_CALL(mock_reporter_, getAllConnections()).Times(2).WillRepeatedly(Invoke([&updates]() { + return updates[0].connections; + })); + + // total + 1: the reconnect triggers an extra full push on top of the normal total sends. + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(total + 1) + .WillRepeatedly(Invoke([this, &updates, &cur](Buffer::InstancePtr& request, bool) { + // cur is still 0 during both the initial connect and the reconnect full push, + // so both correctly validate as full_push=true against updates[0]. + auto resp = validateReq(request, updates[cur], cur == 0); + EXPECT_EQ(resp.request_nonce(), cur + 1); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + client.onServerInitialized(&mock_reporter_); + + // disconnect and reconnect -> sends the full push automatically + callbacks_->onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Unknown, "Testing"); + incTime(config_.connect_retry_interval()); + cur++; // cur becomes 1 only after the reconnect full push has already fired. + + for (; cur < total; cur++) { + client.receiveEvents(updates[cur]); + incTime(config_.default_send_interval()); + } + + int total_sz = 0; + for (int i = 0; i < total; i++) { + total_sz += updates[i].size(); + } + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 2); + EXPECT_EQ(stats_.acks_received_counter_.value(), total + 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), total + 1); + // 6 = 1 (initial full push) + 1 (reconnect full push) + 1+2+1 from updates[1..3]. + EXPECT_EQ(stats_.sent_accepted_cnt_counter_.value(), 6); + EXPECT_EQ(stats_.sent_removed_cnt_counter_.value(), 2); + EXPECT_EQ(stats_.events_dropped_counter_.value(), 0); + // +updates[0].size(): the reconnect full push re-queues the initial connections. + EXPECT_EQ(stats_.queued_updates_counter_.value(), total_sz + updates[0].size()); + EXPECT_EQ(stats_.out_of_order_acks_counter_.value(), 0); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), total); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), total); + EXPECT_EQ(stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::Unknown, + GrpcDisconnectionReason::DisconnectReason::REMOTE_CLOSE)) + .value(), + 1); +} + +// Check edge case -> Disconnect on deadline exceeded +TEST_F(GrpcClientTest, DisconnectOnTooManyUnAckedRequests) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + std::size_t cur = 0; + + ReverseTunnelEvent::TunnelUpdates events; + getStream(1); + + // max_retries + 1: the initial connect sends once, then max_retries timer ticks each send once. + // On the next timer tick send() sees (nonce_current_ - nonce_acked_) > max_retries and + // disconnects before calling sendMessage, so the total successful sends is max_retries + 1. + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(config_.max_retries() + 1) + .WillRepeatedly(Invoke([&events, &cur](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, cur == 0); + EXPECT_EQ(resp.request_nonce(), cur + 1); + + // No ACK is sent. It should eventually disconnect. + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + cur++; + + // max_retries + 2: we need max_retries timer ticks for the sends, plus one more tick + // to trigger the disconnect check. The first send happens on connect (cur=0). + for (; cur < config_.max_retries() + 2; cur++) { + incTime(config_.default_send_interval()); + } + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.acks_received_counter_.value(), 0); + EXPECT_EQ(stats_.send_attempts_counter_.value(), config_.max_retries() + 1); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), config_.max_retries() + 1); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), 0); + EXPECT_EQ(stats_ + .getCounter( + stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::DeadlineExceeded, + GrpcDisconnectionReason::DisconnectReason::MAX_RETRIES_EXCEEDED)) + .value(), + 1); +} + +// Check edge case -> Disconnect with Server on NACK. +TEST_F(GrpcClientTest, DisconnectOnNack) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + ReverseTunnelEvent::TunnelUpdates events; + getStream(1); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .WillOnce(Invoke([this, &events](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, true); + EXPECT_EQ(resp.request_nonce(), 1); + resp.mutable_error_detail()->set_code(Grpc::Status::WellKnownGrpcStatus::Unavailable); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), 1); + EXPECT_EQ( + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::Aborted, + GrpcDisconnectionReason::DisconnectReason::NACK_RECEIVED)) + .value(), + 1); +} + +// Check edge case -> Disconnect on Buffer Full (Also ensure that full push has no limits) +TEST_F(GrpcClientTest, DisconnectOnBufferFull) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + std::vector nodes; + for (std::size_t i = 0; i < config_.max_buffer_count() + 1; i++) { + nodes.push_back("node_" + std::to_string(i)); + } + + ReverseTunnelEvent::TunnelUpdates connect_events{makeConnections(nodes), {}}; + getStream(1); + + EXPECT_CALL(mock_reporter_, getAllConnections()).WillOnce([&connect_events]() { + return connect_events.connections; + }); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .WillOnce(Invoke([this, &connect_events](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, connect_events, true); + EXPECT_EQ(resp.request_nonce(), 1); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + client.receiveEvents(connect_events); + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.events_dropped_counter_.value(), nodes.size()); + EXPECT_EQ(stats_.sent_accepted_cnt_counter_.value(), nodes.size()); + EXPECT_EQ(stats_.sent_removed_cnt_counter_.value(), 0); + EXPECT_EQ(stats_.queued_updates_counter_.value(), nodes.size()); + EXPECT_EQ( + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted, + GrpcDisconnectionReason::DisconnectReason::BUFFER_OVERFLOW)) + .value(), + 1); +} + +// Check edge case -> Prev Nonce ignored +TEST_F(GrpcClientTest, OutOfOrderNonce) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + std::size_t cur = 0; + + ReverseTunnelEvent::TunnelUpdates events; + getStream(1); + + // Send nonce=0 (already acked) for all responses to trigger out-of-order. + // nonce=0 is always <= nonce_acked_ (which starts at 0), so every response lands + // in the else branch and increments out_of_order_acks_counter_. + // Same +1/+2 arithmetic as DisconnectOnTooManyUnAckedRequests: max_retries + 1 sends + // succeed before the disconnect fires. + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(config_.max_retries() + 1) + .WillRepeatedly(Invoke([this, &events, &cur](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, cur == 0); + EXPECT_EQ(resp.request_nonce(), cur + 1); + + resp.set_request_nonce(0); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + cur++; + + for (; cur < config_.max_retries() + 2; cur++) { + incTime(config_.default_send_interval()); + } + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), config_.max_retries() + 1); + EXPECT_EQ(stats_.out_of_order_acks_counter_.value(), config_.max_retries() + 1); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), config_.max_retries() + 1); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), 0); + EXPECT_EQ(stats_ + .getCounter( + stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::DeadlineExceeded, + GrpcDisconnectionReason::DisconnectReason::MAX_RETRIES_EXCEEDED)) + .value(), + 1); +} + +// Check edge case -> Skip Nonce +TEST_F(GrpcClientTest, SkipNonce) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + std::size_t cur = 0; + + ReverseTunnelEvent::TunnelUpdates events; + getStream(1); + + // max_retries + 2: the normal max_retries + 1 sends that would trigger disconnect, + // but a late ACK at iteration max_retries advances nonce_acked_ and buys one more send. + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .Times(config_.max_retries() + 2) + .WillRepeatedly(Invoke([this, &events, &cur](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, events, cur == 0); + EXPECT_EQ(resp.request_nonce(), cur + 1); + + // Only ACK the nonce at iteration max_retries, proving a single late ACK + // advances the watermark and prevents disconnect. + if (cur == config_.max_retries()) { + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + } + })); + + client.onServerInitialized(&mock_reporter_); + cur++; + + for (; cur < config_.max_retries() + 2; cur++) { + incTime(config_.default_send_interval()); + } + + // After the stream is up, retroactively send ACKs for earlier nonces. + // These are all below nonce_acked_ now, so they count as out-of-order. + for (std::size_t i = 1; i <= config_.max_retries(); i++) { + StreamTunnelsResp resp; + resp.set_request_nonce(i); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + } + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), config_.max_retries() + 2); + EXPECT_EQ(stats_.out_of_order_acks_counter_.value(), config_.max_retries()); + EXPECT_EQ(stats_.acks_received_counter_.value(), 1); + EXPECT_EQ(stats_.nonce_current_gauge_.value(), config_.max_retries() + 2); + EXPECT_EQ(stats_.nonce_acked_gauge_.value(), config_.max_retries() + 1); +} + +// Edge case -> Remote close Status Ok +TEST_F(GrpcClientTest, OkRemoteClose) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + ReverseTunnelEvent::TunnelUpdates events; + getStream(1); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .WillOnce(Invoke([&events](Buffer::InstancePtr& request, bool) { + auto response = validateReq(request, events, true); + EXPECT_EQ(response.request_nonce(), 1); + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + callbacks_->onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Ok, "Testing"); + + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 1); + EXPECT_EQ(stats_.send_attempts_counter_.value(), 1); + EXPECT_EQ(stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::Ok, + GrpcDisconnectionReason::DisconnectReason::REMOTE_CLOSE)) + .value(), + 1); +} + +// --- Hardening tests --- + +TEST_F(GrpcClientTest, ReceiveEventsBeforeInitialized) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + ReverseTunnelEvent::TunnelUpdates events{makeConnections({"node_1"}), {}}; + client.receiveEvents(std::move(events)); + + EXPECT_EQ(stats_.queued_updates_counter_.value(), 0); + EXPECT_EQ(stats_.events_dropped_counter_.value(), 0); +} + +TEST_F(GrpcClientTest, ClusterNotFoundLogsAndReturns) { + GrpcClient client{context_, config_}; + + EXPECT_CALL(cm_, getThreadLocalCluster(_)).WillOnce(Return(nullptr)); + + client.onServerInitialized(&mock_reporter_); + + ReverseTunnelEvent::TunnelUpdates events{makeConnections({"node_1"}), {}}; + client.receiveEvents(std::move(events)); + + auto stats_{getStats()}; + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 0); + EXPECT_EQ(stats_.queued_updates_counter_.value(), 0); +} + +TEST_F(GrpcClientTest, ClientCreationFailureLogsAndReturns) { + GrpcClient client{context_, config_}; + + EXPECT_CALL(manager_, getOrCreateRawAsyncClient(_, _, _)) + .WillOnce(Return(absl::InvalidArgumentError("Bad Karma"))); + + client.onServerInitialized(&mock_reporter_); + + ReverseTunnelEvent::TunnelUpdates events{makeConnections({"node_1"}), {}}; + client.receiveEvents(std::move(events)); + + auto stats_{getStats()}; + EXPECT_EQ(stats_.connection_attempts_counter_.value(), 0); + EXPECT_EQ(stats_.queued_updates_counter_.value(), 0); +} + +TEST_F(GrpcClientTest, BufferOverflowWhileDisconnectedDoesNotRearmRetry) { + GrpcClient client{context_, config_}; + auto stats_{getStats()}; + + std::vector nodes; + for (std::size_t i = 0; i < config_.max_buffer_count() + 1; i++) { + nodes.push_back("node_" + std::to_string(i)); + } + + ReverseTunnelEvent::TunnelUpdates big_update{makeConnections(nodes), {}}; + getStream(1); + + EXPECT_CALL(mock_reporter_, getAllConnections()).WillOnce(Return(big_update.connections)); + + EXPECT_CALL(*async_stream_, sendMessageRaw_(_, false)) + .WillOnce(Invoke([this, &big_update](Buffer::InstancePtr& request, bool) { + auto resp = validateReq(request, big_update, true); + callbacks_->onReceiveMessageRaw(Grpc::Common::serializeMessage(resp)); + })); + + EXPECT_CALL(*async_stream_, resetStream()); + + client.onServerInitialized(&mock_reporter_); + + // First overflow: stream is alive, should disconnect. + client.receiveEvents(ReverseTunnelEvent::TunnelUpdates{makeConnections(nodes), {}}); + EXPECT_EQ( + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted, + GrpcDisconnectionReason::DisconnectReason::BUFFER_OVERFLOW)) + .value(), + 1); + + // Second overflow: stream is null, should NOT increment disconnect counter. + client.receiveEvents(ReverseTunnelEvent::TunnelUpdates{makeConnections(nodes), {}}); + EXPECT_EQ( + stats_ + .getCounter(stats_.disconnects_, + stats_.getTags(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted, + GrpcDisconnectionReason::DisconnectReason::BUFFER_OVERFLOW)) + .value(), + 1); + + // Events still counted as dropped both times. + EXPECT_EQ(stats_.events_dropped_counter_.value(), nodes.size() * 2); +} + +// Verify default values when proto fields are unset/zero. +TEST_F(GrpcClientTest, ConfigDefaults) { + GrpcConfigProto bare_config; + bare_config.set_cluster("test_cluster"); + + GrpcClientConfig parsed(bare_config); + + EXPECT_EQ(parsed.stat_prefix, "reverse_tunnel_reporter_client.grpc_client"); + EXPECT_EQ(parsed.cluster, "test_cluster"); + EXPECT_EQ(parsed.send_interval.count(), 5000); + EXPECT_EQ(parsed.connect_retry_interval.count(), 5000); + EXPECT_EQ(parsed.max_retries, 5); + EXPECT_EQ(parsed.max_buffer, 1000000); +} + +class GrpcClientFactoryTest : public testing::Test { +public: + void SetUp() override { + factory_ = Registry::FactoryRegistry::getFactory( + "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.clients.grpc_client"); + ASSERT_NE(nullptr, factory_); + + ON_CALL(context_, messageValidationVisitor()) + .WillByDefault(ReturnRef(ProtobufMessage::getStrictValidationVisitor())); + } + +protected: + ReverseTunnelReporterClientFactory* factory_{}; + NiceMock context_; +}; + +TEST_F(GrpcClientFactoryTest, Name) { + EXPECT_EQ("envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.clients.grpc_client", + factory_->name()); +} + +TEST_F(GrpcClientFactoryTest, CreateEmptyConfigProto) { + auto config = factory_->createEmptyConfigProto(); + EXPECT_NE(nullptr, config); + EXPECT_NE(nullptr, dynamic_cast(config.get())); +} + +TEST_F(GrpcClientFactoryTest, CreateClientReturnsNonNull) { + Api::ApiPtr api = Api::createApiForTest(); + Event::DispatcherPtr dispatcher = api->allocateDispatcher("test"); + ON_CALL(context_, mainThreadDispatcher()).WillByDefault(ReturnRef(*dispatcher)); + Stats::IsolatedStoreImpl stats_store; + ON_CALL(context_, scope()).WillByDefault(ReturnRef(*stats_store.rootScope())); + + GrpcConfigProto config; + config.set_cluster("test_cluster"); + + auto client = factory_->createClient(context_, config); + EXPECT_NE(nullptr, client); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/test/clients/integration_test_utils.h b/contrib/reverse_tunnel_reporter/test/clients/integration_test_utils.h new file mode 100644 index 0000000000000..85d2267d043bb --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/clients/integration_test_utils.h @@ -0,0 +1,231 @@ +#pragma once + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.pb.h" +#include "envoy/extensions/filters/http/router/v3/router.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/extensions/filters/network/reverse_tunnel/v3/reverse_tunnel.pb.h" + +#include "source/common/common/fmt.h" + +#include "test/config/utility.h" + +#include "contrib/envoy/extensions/reverse_tunnel_reporters/v3alpha/reporters/event_reporter.pb.h" + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +constexpr absl::string_view downstreamExtension = + "envoy.bootstrap.reverse_tunnel.downstream_socket_interface"; +constexpr absl::string_view upstreamExtension = + "envoy.bootstrap.reverse_tunnel.upstream_socket_interface"; +constexpr absl::string_view downstreamCluster = "downstreamCluster"; +constexpr absl::string_view upstreamCluster = "upstreamCluster"; +constexpr absl::string_view upstreamListener = "upstreamListener"; +constexpr absl::string_view upstreamFilter = "envoy.filters.network.reverse_tunnel"; +constexpr absl::string_view downstreamTenant = "downstreamTenant"; +constexpr absl::string_view downstreamResolver = "envoy.resolvers.reverse_connection"; + +constexpr int upstreamPort = 9000; +constexpr int reportingPort = 8082; + +using envoy::config::bootstrap::v3::Bootstrap; +using envoy::config::cluster::v3::Cluster; +using envoy::config::core::v3::TypedExtensionConfig; +using envoy::config::listener::v3::Listener; +using envoy::extensions::bootstrap::reverse_tunnel::downstream_socket_interface::v3:: + DownstreamReverseConnectionSocketInterface; +using envoy::extensions::bootstrap::reverse_tunnel::upstream_socket_interface::v3:: + UpstreamReverseConnectionSocketInterface; +using envoy::extensions::filters::network::reverse_tunnel::v3::ReverseTunnel; +using envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig; + +inline TypedExtensionConfig getDownstreamExtension() { + DownstreamReverseConnectionSocketInterface cfg; + cfg.set_stat_prefix(downstreamExtension); + cfg.set_enable_detailed_stats(true); + + TypedExtensionConfig ext; + ext.set_name(downstreamExtension); + std::ignore = ext.mutable_typed_config()->PackFrom(cfg); + + return ext; +} + +inline TypedExtensionConfig getUpstreamExtension(const EventReporterConfig& config, + bool enable_tenant_isolation) { + UpstreamReverseConnectionSocketInterface cfg; + cfg.set_stat_prefix(upstreamExtension); + cfg.set_enable_detailed_stats(true); + cfg.mutable_enable_tenant_isolation()->set_value(enable_tenant_isolation); + const std::string reporterName = + "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.reporters.event_reporter"; + auto* reporter = cfg.mutable_reporter_config(); + reporter->set_name(reporterName); + std::ignore = reporter->mutable_typed_config()->PackFrom(config); + + TypedExtensionConfig ext; + ext.set_name(upstreamExtension); + std::ignore = ext.mutable_typed_config()->PackFrom(cfg); + + return ext; +} + +inline Cluster getDownstreamCluster(absl::string_view localhost) { + Cluster cluster; + cluster.set_name(downstreamCluster); + cluster.set_type(Cluster::STATIC); + cluster.mutable_connect_timeout()->set_seconds(30); + cluster.mutable_load_assignment()->set_cluster_name(downstreamCluster); + + auto* sa = cluster.mutable_load_assignment() + ->mutable_endpoints() + ->Add() + ->add_lb_endpoints() + ->mutable_endpoint() + ->mutable_address() + ->mutable_socket_address(); + + sa->set_address(localhost); + sa->set_port_value(upstreamPort); + + return cluster; +} + +inline Cluster getUpstreamCluster(absl::string_view localhost) { + Cluster cluster; + cluster.set_name(upstreamCluster); + cluster.set_type(Cluster::STATIC); + cluster.mutable_connect_timeout()->set_seconds(30); + cluster.mutable_load_assignment()->set_cluster_name(upstreamCluster); + + auto* sa = cluster.mutable_load_assignment() + ->mutable_endpoints() + ->Add() + ->add_lb_endpoints() + ->mutable_endpoint() + ->mutable_address() + ->mutable_socket_address(); + + sa->set_address(localhost); + sa->set_port_value(reportingPort); + + return cluster; +} + +inline Listener getUpstreamListener(absl::string_view anyhost) { + Listener listener; + listener.set_name(upstreamListener); + listener.set_stat_prefix(upstreamListener); + + auto* sa = listener.mutable_address()->mutable_socket_address(); + sa->set_address(anyhost); + sa->set_port_value(upstreamPort); + + auto* filter = listener.add_filter_chains()->add_filters(); + filter->set_name(upstreamFilter); + + ReverseTunnel rtFilter; + rtFilter.mutable_ping_interval()->set_seconds(300); // No ping timeouts. + + std::ignore = filter->mutable_typed_config()->PackFrom(rtFilter); + + return listener; +} + +inline Listener getDownstreamListener(const std::string& name, int num_listeners) { + Listener listener; + listener.set_name(name); + + auto* sa = listener.mutable_address()->mutable_socket_address(); + sa->set_address(fmt::format("rc://{}:{}:{}@{}:{}", name, downstreamCluster, downstreamTenant, + downstreamCluster, num_listeners)); + sa->set_port_value(0); + sa->set_resolver_name(downstreamResolver); + + listener.mutable_listener_filters_timeout()->set_seconds(0); + + auto* filter_chain = listener.add_filter_chains(); + auto* filter = filter_chain->add_filters(); + filter->set_name("envoy.filters.network.http_connection_manager"); + + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager hcm; + hcm.set_stat_prefix(name); + + auto* route_config = hcm.mutable_route_config(); + auto* vh = route_config->add_virtual_hosts(); + vh->set_name(name); + vh->add_domains("*"); + + auto* route = vh->add_routes(); + route->mutable_match()->set_prefix("/"); + auto* direct_response = route->mutable_direct_response(); + direct_response->set_status(200); + direct_response->mutable_body()->set_inline_string("reverse connection listener OK"); + + auto* http_filter = hcm.add_http_filters(); + http_filter->set_name("envoy.filters.http.router"); + envoy::extensions::filters::http::router::v3::Router router_cfg; + std::ignore = http_filter->mutable_typed_config()->PackFrom(router_cfg); + + std::ignore = filter->mutable_typed_config()->PackFrom(hcm); + + return listener; +} + +void addCluster(Cluster&& cluster, ConfigHelper& config_helper) { + config_helper.addConfigModifier([cluster = std::move(cluster)](Bootstrap& bootstrap) { + *(bootstrap.mutable_static_resources()->mutable_clusters()->Add()) = cluster; + }); +} + +void addListener(Listener&& listener, ConfigHelper& config_helper, int& current) { + config_helper.addConfigModifier([listener = std::move(listener)](Bootstrap& bootstrap) { + *(bootstrap.mutable_static_resources()->mutable_listeners()->Add()) = listener; + }); + + ++current; +} + +void addBootstrapExtension(TypedExtensionConfig&& extension, ConfigHelper& config_helper) { + config_helper.addConfigModifier([extension = std::move(extension)](Bootstrap& bootstrap) { + *(bootstrap.mutable_bootstrap_extensions()->Add()) = extension; + }); +} + +void removeListener(const std::string& name, ConfigHelper& config_helper, int& current) { + config_helper.addConfigModifier([&name](Bootstrap& bootstrap) { + auto* listeners = bootstrap.mutable_static_resources()->mutable_listeners(); + + for (int i = 0; i < listeners->size(); ++i) { + if (listeners->Get(i).name() == name) { + listeners->DeleteSubrange(i, 1); + break; + } + } + }); + + --current; +} + +Cluster getHttp2Cluster(Cluster& cluster) { + ConfigHelper::HttpProtocolOptions http2_options; + http2_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + + std::ignore = (*cluster.mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(http2_options); + + return cluster; +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/reverse_tunnel_reporter/test/reporters/BUILD b/contrib/reverse_tunnel_reporter/test/reporters/BUILD new file mode 100644 index 0000000000000..991e168a0ae8e --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/reporters/BUILD @@ -0,0 +1,24 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "event_reporter_test", + srcs = ["event_reporter_test.cc"], + deps = [ + "//contrib/reverse_tunnel_reporter/source/reporters/event_reporter:event_reporter_lib", + "//source/common/api:api_lib", + "//source/common/config:utility_lib", + "//test/mocks:common_lib", + "//test/mocks/server:server_mocks", + "//test/test_common:registry_lib", + "//test/test_common:test_time_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/contrib/reverse_tunnel_reporter/test/reporters/event_reporter_test.cc b/contrib/reverse_tunnel_reporter/test/reporters/event_reporter_test.cc new file mode 100644 index 0000000000000..9251b836293a1 --- /dev/null +++ b/contrib/reverse_tunnel_reporter/test/reporters/event_reporter_test.cc @@ -0,0 +1,622 @@ +#include + +#include "source/common/api/api_impl.h" + +#include "test/mocks/common.h" +#include "test/mocks/server/server_factory_context.h" +#include "test/test_common/registry.h" +#include "test/test_common/test_time.h" +#include "test/test_common/utility.h" + +#include "contrib/reverse_tunnel_reporter/source/reporters/event_reporter/factory.h" +#include "contrib/reverse_tunnel_reporter/source/reporters/event_reporter/reporter.h" +#include "contrib/reverse_tunnel_reporter/source/reverse_tunnel_event_types.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Invoke; +using testing::NiceMock; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Bootstrap { +namespace ReverseConnection { + +class MockReverseTunnelReporterClient : public ReverseTunnelReporterClient { +public: + MockReverseTunnelReporterClient() = default; + ~MockReverseTunnelReporterClient() override = default; + + MOCK_METHOD(void, onServerInitialized, (ReverseTunnelReporterWithState*), (override)); + MOCK_METHOD(void, receiveEvents, (ReverseTunnelEvent::TunnelUpdates), (override)); +}; + +class EventReporterTest : public testing::Test { +protected: + void SetUp() override { + api_ = Api::createApiForTest(); + dispatcher_ = api_->allocateDispatcher("test_thread"); + + ON_CALL(context_, mainThreadDispatcher()).WillByDefault(ReturnRef(*dispatcher_)); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(*stats_store_.rootScope())); + ON_CALL(context_, messageValidationVisitor()) + .WillByDefault(ReturnRef(ProtobufMessage::getStrictValidationVisitor())); + + auto mock_client1 = std::make_unique>(); + auto mock_client2 = std::make_unique>(); + mock_client1_ = mock_client1.get(); + mock_client2_ = mock_client2.get(); + + std::vector clients; + clients.push_back(std::move(mock_client1)); + clients.push_back(std::move(mock_client2)); + + EventReporter::ConfigProto config; + config.set_stat_prefix("test_prefix"); + + reporter_ = std::make_unique(context_, config, std::move(clients)); + } + + void createTestConnection(const std::string& node_id, const std::string& cluster_id, + const std::string& tenant_id = "tenant1", + int64_t initiation_time_ms = 0) { + reporter_->reportConnectionEvent(node_id, cluster_id, tenant_id, initiation_time_ms); + } + + void createTestDisconnection(const std::string& node_id, const std::string& cluster_id) { + reporter_->reportDisconnectionEvent(node_id, cluster_id); + } + + void runDispatcher() { dispatcher_->run(Event::Dispatcher::RunType::NonBlock); } + + uint64_t getCounterValue(const std::string& name) { + return stats_store_.counterFromString("test_prefix." + name).value(); + } + + uint64_t getGaugeValue(const std::string& name) { + return stats_store_.gaugeFromString("test_prefix." + name, Stats::Gauge::ImportMode::Accumulate) + .value(); + } + + Api::ApiPtr api_; + Event::DispatcherPtr dispatcher_; + NiceMock context_; + Stats::TestUtil::TestStore stats_store_; + NiceMock* mock_client1_; + NiceMock* mock_client2_; + std::unique_ptr reporter_; +}; + +TEST_F(EventReporterTest, AddRemoveConnections) { + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .Times(4) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node1", updates.connections[0]->node_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node2", updates.connections[0]->node_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(0, updates.connections.size()); + EXPECT_EQ(1, updates.disconnections.size()); + EXPECT_EQ(ReverseTunnelEvent::getName("node1"), updates.disconnections[0]->name); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(0, updates.connections.size()); + EXPECT_EQ(1, updates.disconnections.size()); + EXPECT_EQ(ReverseTunnelEvent::getName("node2"), updates.disconnections[0]->name); + })); + + EXPECT_CALL(*mock_client2_, receiveEvents(_)).Times(4); + + createTestConnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(1, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + createTestConnection("node2", "cluster2"); + runDispatcher(); + + EXPECT_EQ(2, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(2, connections.size()); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(2, connections.size()); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(1, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + createTestDisconnection("node2", "cluster2"); + runDispatcher(); + + EXPECT_EQ(2, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_full_pulls_total")); +} + +TEST_F(EventReporterTest, DuplicateConnectionHandling) { + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .Times(2) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node1", updates.connections[0]->node_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(0, updates.connections.size()); + EXPECT_EQ(1, updates.disconnections.size()); + EXPECT_EQ(ReverseTunnelEvent::getName("node1"), updates.disconnections[0]->name); + })); + + EXPECT_CALL(*mock_client2_, receiveEvents(_)).Times(2); + + createTestConnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(1, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + createTestConnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(2, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(1, connections.size()); + EXPECT_EQ("node1", connections[0]->node_id); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(1, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(1, connections.size()); + EXPECT_EQ("node1", connections[0]->node_id); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(2, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_full_pulls_total")); +} + +TEST_F(EventReporterTest, PullsBeforeConnectionEvents) { + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_full_pulls_total")); +} + +TEST_F(EventReporterTest, RemoveNonExistentConnection) { + Envoy::Logger::Registry::setLogLevel(spdlog::level::warn); + MockLogSink sink(Envoy::Logger::Registry::getSink()); + + EXPECT_CALL(sink, log(_, _)) + .WillOnce(Invoke([](absl::string_view, const spdlog::details::log_msg& msg) { + EXPECT_EQ(spdlog::level::warn, msg.level); + })); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)).Times(0); + EXPECT_CALL(*mock_client2_, receiveEvents(_)).Times(0); + + createTestDisconnection("nonexistent", "connection"); + runDispatcher(); + + EXPECT_EQ(0, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_unique_active")); + + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node1", updates.connections[0]->node_id); + })); + EXPECT_CALL(*mock_client2_, receiveEvents(_)); + + createTestConnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(1, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); +} + +TEST_F(EventReporterTest, OnServerInitialized) { + Envoy::Logger::Registry::setLogLevel(spdlog::level::info); + MockLogSink sink(Envoy::Logger::Registry::getSink()); + + EXPECT_CALL(sink, log(_, _)) + .WillOnce(Invoke([](absl::string_view, const spdlog::details::log_msg& msg) { + EXPECT_EQ(spdlog::level::info, msg.level); + })); + + EXPECT_CALL(*mock_client1_, onServerInitialized(_)); + EXPECT_CALL(*mock_client2_, onServerInitialized(_)); + + reporter_->onServerInitialized(); + + EXPECT_EQ(0, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(0, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(0, getCounterValue("reverse_tunnel_full_pulls_total")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_unique_active")); +} + +TEST_F(EventReporterTest, DefaultStatPrefix) { + EventReporter::ConfigProto config; + + std::vector clients; + auto mock_client1 = std::make_unique>(); + auto mock_client2 = std::make_unique>(); + mock_client1_ = mock_client1.get(); + mock_client2_ = mock_client2.get(); + clients.push_back(std::move(mock_client1)); + clients.push_back(std::move(mock_client2)); + + auto default_reporter = std::make_unique(context_, config, std::move(clients)); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)); + EXPECT_CALL(*mock_client2_, receiveEvents(_)); + + default_reporter->reportConnectionEvent("node1", "cluster1", "tenant1", 0); + runDispatcher(); + + EXPECT_EQ( + 1, stats_store_.counterFromString("reverse_tunnel_reporter.reverse_tunnel_established_total") + .value()); + EXPECT_EQ(1, stats_store_ + .gaugeFromString("reverse_tunnel_reporter.reverse_tunnel_active", + Stats::Gauge::ImportMode::Accumulate) + .value()); + EXPECT_EQ(1, stats_store_ + .gaugeFromString("reverse_tunnel_reporter.reverse_tunnel_unique_active", + Stats::Gauge::ImportMode::Accumulate) + .value()); + + auto connections = default_reporter->getAllConnections(); + EXPECT_EQ(1, connections.size()); + EXPECT_EQ( + 1, stats_store_.counterFromString("reverse_tunnel_reporter.reverse_tunnel_full_pulls_total") + .value()); +} + +TEST_F(EventReporterTest, MixedScenario) { + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .Times(4) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node1", updates.connections[0]->node_id); + EXPECT_EQ("tenant_A", updates.connections[0]->tenant_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node2", updates.connections[0]->node_id); + EXPECT_EQ("tenant_B", updates.connections[0]->tenant_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(0, updates.connections.size()); + EXPECT_EQ(1, updates.disconnections.size()); + EXPECT_EQ(ReverseTunnelEvent::getName("node2"), updates.disconnections[0]->name); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node3", updates.connections[0]->node_id); + EXPECT_EQ("tenant_C", updates.connections[0]->tenant_id); + })); + + EXPECT_CALL(*mock_client2_, receiveEvents(_)).Times(4); + + createTestConnection("node1", "cluster1", "tenant_A"); + runDispatcher(); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + createTestConnection("node2", "cluster2", "tenant_B"); + runDispatcher(); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(2, connections.size()); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestConnection("node1", "cluster1", "tenant_A"); + runDispatcher(); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(3, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + createTestConnection("node2", "cluster2", "tenant_B"); + runDispatcher(); + EXPECT_EQ(4, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(4, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(3, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(2, connections.size()); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestDisconnection("node2", "cluster2"); + runDispatcher(); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + createTestDisconnection("node2", "cluster2"); + runDispatcher(); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + createTestConnection("node3", "cluster3", "tenant_C"); + runDispatcher(); + EXPECT_EQ(5, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(2, connections.size()); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_full_pulls_total")); + + EXPECT_EQ(5, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(2, getGaugeValue("reverse_tunnel_unique_active")); +} + +TEST_F(EventReporterTest, LargeDuplicateCount) { + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .Times(2) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(1, updates.connections.size()); + EXPECT_EQ(0, updates.disconnections.size()); + EXPECT_EQ("node1", updates.connections[0]->node_id); + EXPECT_EQ("tenant_A", updates.connections[0]->tenant_id); + })) + .WillOnce(Invoke([](const ReverseTunnelEvent::TunnelUpdates& updates) { + EXPECT_EQ(0, updates.connections.size()); + EXPECT_EQ(1, updates.disconnections.size()); + EXPECT_EQ(ReverseTunnelEvent::getName("node1"), updates.disconnections[0]->name); + })); + + EXPECT_CALL(*mock_client2_, receiveEvents(_)).Times(2); + + const int DUPLICATE_COUNT = 50; + + for (int i = 0; i < DUPLICATE_COUNT; i++) { + createTestConnection("node1", "cluster1", "tenant_A"); + runDispatcher(); + } + + EXPECT_EQ(DUPLICATE_COUNT, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(DUPLICATE_COUNT, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + auto connections = reporter_->getAllConnections(); + EXPECT_EQ(1, connections.size()); + EXPECT_EQ(1, getCounterValue("reverse_tunnel_full_pulls_total")); + + for (int i = 0; i < DUPLICATE_COUNT - 1; i++) { + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + } + + EXPECT_EQ(DUPLICATE_COUNT - 1, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(1, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(1, connections.size()); + EXPECT_EQ(2, getCounterValue("reverse_tunnel_full_pulls_total")); + + createTestDisconnection("node1", "cluster1"); + runDispatcher(); + + EXPECT_EQ(DUPLICATE_COUNT, getCounterValue("reverse_tunnel_established_total")); + EXPECT_EQ(DUPLICATE_COUNT, getCounterValue("reverse_tunnel_closed_total")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_active")); + EXPECT_EQ(0, getGaugeValue("reverse_tunnel_unique_active")); + + connections = reporter_->getAllConnections(); + EXPECT_EQ(0, connections.size()); + EXPECT_EQ(3, getCounterValue("reverse_tunnel_full_pulls_total")); +} + +TEST_F(EventReporterTest, InitiationTimeUsedAsCreatedAt) { + // When a valid initiation_time_ms is provided, the Connected event's created_at + // should reflect the DP-side timestamp, not the local wall clock. + const int64_t dp_timestamp_ms = 1700000000000; // 2023-11-14T22:13:20Z + const auto expected_time = Envoy::SystemTime(std::chrono::milliseconds(dp_timestamp_ms)); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .WillOnce(Invoke([&expected_time](const ReverseTunnelEvent::TunnelUpdates& updates) { + ASSERT_EQ(1, updates.connections.size()); + EXPECT_EQ(expected_time, updates.connections[0]->created_at); + EXPECT_EQ("node1", updates.connections[0]->node_id); + })); + EXPECT_CALL(*mock_client2_, receiveEvents(_)); + + createTestConnection("node1", "cluster1", "tenant1", dp_timestamp_ms); + runDispatcher(); +} + +TEST_F(EventReporterTest, ZeroInitiationTimeFallsBackToTimeSource) { + // When initiation_time_ms is 0 (header absent), created_at should come from + // the injected time source, not the raw wall clock. + const auto before = context_.timeSource().systemTime(); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .WillOnce(Invoke([&before](const ReverseTunnelEvent::TunnelUpdates& updates) { + ASSERT_EQ(1, updates.connections.size()); + EXPECT_GE(updates.connections[0]->created_at, before); + EXPECT_LE(updates.connections[0]->created_at, before + std::chrono::seconds(5)); + })); + EXPECT_CALL(*mock_client2_, receiveEvents(_)); + + createTestConnection("node1", "cluster1", "tenant1", 0); + runDispatcher(); +} + +TEST_F(EventReporterTest, OverflowInitiationTimeFallsBackToTimeSource) { + // A maliciously large initiation_time_ms (e.g. int64 max) would overflow when converted into + // SystemTime's finer-grained duration. It must be rejected and fall back to the injected time + // source rather than triggering undefined behavior. + const auto before = context_.timeSource().systemTime(); + + EXPECT_CALL(*mock_client1_, receiveEvents(_)) + .WillOnce(Invoke([&before](const ReverseTunnelEvent::TunnelUpdates& updates) { + ASSERT_EQ(1, updates.connections.size()); + EXPECT_GE(updates.connections[0]->created_at, before); + EXPECT_LE(updates.connections[0]->created_at, before + std::chrono::seconds(5)); + })); + EXPECT_CALL(*mock_client2_, receiveEvents(_)); + + createTestConnection("node1", "cluster1", "tenant1", std::numeric_limits::max()); + runDispatcher(); +} + +// --- Factory tests --- + +class MockReverseTunnelReporterClientFactory : public ReverseTunnelReporterClientFactory { +public: + MOCK_METHOD(ReverseTunnelReporterClientPtr, createClient, + (Server::Configuration::ServerFactoryContext&, const Protobuf::Message&), (override)); + + std::string name() const override { return "mock_client_factory"; } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + +class EventReporterFactoryTest : public testing::Test { +protected: + void SetUp() override { + ON_CALL(context_, messageValidationVisitor()) + .WillByDefault(ReturnRef(ProtobufMessage::getStrictValidationVisitor())); + ON_CALL(context_, scope()).WillByDefault(ReturnRef(*stats_store_.rootScope())); + + api_ = Api::createApiForTest(); + dispatcher_ = api_->allocateDispatcher("test_thread"); + ON_CALL(context_, mainThreadDispatcher()).WillByDefault(ReturnRef(*dispatcher_)); + } + + Api::ApiPtr api_; + Event::DispatcherPtr dispatcher_; + NiceMock context_; + Stats::TestUtil::TestStore stats_store_; + EventReporterFactory factory_; +}; + +TEST_F(EventReporterFactoryTest, Name) { + EXPECT_EQ( + "envoy.extensions.reverse_tunnel.reverse_tunnel_reporting_service.reporters.event_reporter", + factory_.name()); +} + +TEST_F(EventReporterFactoryTest, CreateEmptyConfigProto) { + auto config = factory_.createEmptyConfigProto(); + EXPECT_NE(nullptr, config); + EXPECT_NE( + nullptr, + dynamic_cast< + envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig*>( + config.get())); +} + +TEST_F(EventReporterFactoryTest, CreateReporterWithRegisteredClient) { + MockReverseTunnelReporterClientFactory mock_client_factory; + Registry::InjectFactory registered(mock_client_factory); + + EXPECT_CALL(mock_client_factory, createClient(_, _)) + .WillOnce(Invoke([](Server::Configuration::ServerFactoryContext&, + const Protobuf::Message&) -> ReverseTunnelReporterClientPtr { + return std::make_unique>(); + })); + + auto config = factory_.createEmptyConfigProto(); + auto& reporter_config = dynamic_cast< + envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig&>( + *config); + reporter_config.set_stat_prefix("test"); + + auto* client_entry = reporter_config.add_clients(); + client_entry->set_name("mock_client_factory"); + std::ignore = client_entry->mutable_typed_config()->PackFrom(Protobuf::Struct()); + + auto reporter = factory_.createReporter(context_, std::move(config)); + EXPECT_NE(nullptr, reporter); +} + +TEST_F(EventReporterFactoryTest, CreateClientWithUnknownFactoryThrows) { + auto config = factory_.createEmptyConfigProto(); + auto& reporter_config = dynamic_cast< + envoy::extensions::reverse_tunnel_reporters::v3alpha::reporters::EventReporterConfig&>( + *config); + reporter_config.set_stat_prefix("test"); + + auto* client_entry = reporter_config.add_clients(); + client_entry->set_name("nonexistent_client_factory"); + std::ignore = client_entry->mutable_typed_config()->PackFrom(Protobuf::Struct()); + + EXPECT_THROW_WITH_REGEX(factory_.createReporter(context_, std::move(config)), EnvoyException, + "Unknown Reporter Client Factory: 'nonexistent_client_factory'"); +} + +} // namespace ReverseConnection +} // namespace Bootstrap +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/rocketmq_proxy/filters/network/source/BUILD b/contrib/rocketmq_proxy/filters/network/source/BUILD index 460d421dc9003..1f56a6df881cd 100644 --- a/contrib/rocketmq_proxy/filters/network/source/BUILD +++ b/contrib/rocketmq_proxy/filters/network/source/BUILD @@ -141,6 +141,5 @@ envoy_cc_library( hdrs = ["metadata.h"], deps = [ "//source/common/http:header_map_lib", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/contrib/rocketmq_proxy/filters/network/source/active_message.h b/contrib/rocketmq_proxy/filters/network/source/active_message.h index bdf5d594181a6..cb9d1d2d178f5 100644 --- a/contrib/rocketmq_proxy/filters/network/source/active_message.h +++ b/contrib/rocketmq_proxy/filters/network/source/active_message.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/event/deferred_deletable.h" #include "envoy/network/connection.h" #include "envoy/network/filter.h" @@ -9,7 +11,6 @@ #include "source/common/common/linked_object.h" #include "source/common/common/logger.h" -#include "absl/types/optional.h" #include "contrib/rocketmq_proxy/filters/network/source/codec.h" #include "contrib/rocketmq_proxy/filters/network/source/protocol.h" #include "contrib/rocketmq_proxy/filters/network/source/router/router.h" @@ -91,7 +92,7 @@ class ActiveMessage : public LinkedObject, RemotingCommandPtr response_; MessageMetadataSharedPtr metadata_; Router::RouterPtr router_; - absl::optional cached_route_; + std::optional cached_route_; void updateActiveRequestStats(bool is_inc = true); }; diff --git a/contrib/rocketmq_proxy/filters/network/source/metadata.h b/contrib/rocketmq_proxy/filters/network/source/metadata.h index a2efeb031bbc5..b1ba4127aff2b 100644 --- a/contrib/rocketmq_proxy/filters/network/source/metadata.h +++ b/contrib/rocketmq_proxy/filters/network/source/metadata.h @@ -1,11 +1,10 @@ #pragma once +#include #include #include "source/common/http/header_map_impl.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -30,7 +29,7 @@ class MessageMetadata { private: bool is_oneway_{false}; - absl::optional topic_name_{}; + std::optional topic_name_; Http::HeaderMapPtr headers_{Http::RequestHeaderMapImpl::create()}; }; diff --git a/contrib/rocketmq_proxy/filters/network/test/BUILD b/contrib/rocketmq_proxy/filters/network/test/BUILD index 52533527f0db2..2a394d9eeba4a 100644 --- a/contrib/rocketmq_proxy/filters/network/test/BUILD +++ b/contrib/rocketmq_proxy/filters/network/test/BUILD @@ -73,7 +73,6 @@ envoy_cc_test( "//test/common/upstream:utility_lib", "//test/mocks/network:network_mocks", "//test/mocks/server:factory_context_mocks", - "//test/mocks/server:instance_mocks", "//test/mocks/stream_info:stream_info_mocks", "//test/test_common:utility_lib", ], @@ -102,7 +101,6 @@ envoy_cc_test( "//contrib/rocketmq_proxy/filters/network/source:config", "//test/mocks/local_info:local_info_mocks", "//test/mocks/server:factory_context_mocks", - "//test/mocks/server:instance_mocks", "//test/test_common:registry_lib", "@envoy_api//contrib/envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", ], diff --git a/contrib/rocketmq_proxy/filters/network/test/config_test.cc b/contrib/rocketmq_proxy/filters/network/test/config_test.cc index c0900b0bdeecd..7b6e577aa6411 100644 --- a/contrib/rocketmq_proxy/filters/network/test/config_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/config_test.cc @@ -1,6 +1,5 @@ #include "test/mocks/local_info/mocks.h" #include "test/mocks/server/factory_context.h" -#include "test/mocks/server/instance.h" #include "test/test_common/registry.h" #include "contrib/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" diff --git a/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc b/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc index 2cbd77621f6f9..1c5d64001cafb 100644 --- a/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/conn_manager_test.cc @@ -5,7 +5,6 @@ #include "test/mocks/network/connection.h" #include "test/mocks/network/mocks.h" #include "test/mocks/server/factory_context.h" -#include "test/mocks/server/instance.h" #include "contrib/rocketmq_proxy/filters/network/source/config.h" #include "contrib/rocketmq_proxy/filters/network/source/conn_manager.h" diff --git a/contrib/rocketmq_proxy/filters/network/test/router_test.cc b/contrib/rocketmq_proxy/filters/network/test/router_test.cc index 9581241503ee7..df9a6ef54a7ba 100644 --- a/contrib/rocketmq_proxy/filters/network/test/router_test.cc +++ b/contrib/rocketmq_proxy/filters/network/test/router_test.cc @@ -1,3 +1,5 @@ +#include + #include "test/mocks/server/factory_context.h" #include "contrib/rocketmq_proxy/filters/network/source/config.h" @@ -253,7 +255,7 @@ TEST_F(RocketmqRouterTest, NoHealthyHosts) { })); EXPECT_CALL(context_.server_factory_context_.cluster_manager_.thread_local_cluster_, tcpConnPool(_, _)) - .WillOnce(Return(absl::nullopt)); + .WillOnce(Return(std::nullopt)); EXPECT_CALL(*active_message_, onReset()); startRequest(); diff --git a/contrib/sip_proxy/filters/network/source/BUILD b/contrib/sip_proxy/filters/network/source/BUILD index ddbec711b38f8..833df9339d365 100644 --- a/contrib/sip_proxy/filters/network/source/BUILD +++ b/contrib/sip_proxy/filters/network/source/BUILD @@ -117,7 +117,6 @@ envoy_cc_library( "//envoy/buffer:buffer_interface", "//source/common/common:macros", "//source/common/http:header_map_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//contrib/envoy/extensions/filters/network/sip_proxy/v3alpha:pkg_cc_proto", ], ) @@ -149,7 +148,6 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/protobuf", "//source/common/singleton:const_singleton", - "@abseil-cpp//absl/types:optional", "@envoy_api//contrib/envoy/extensions/filters/network/sip_proxy/tra/v3alpha:pkg_cc_proto", "@envoy_api//contrib/envoy/extensions/filters/network/sip_proxy/v3alpha:pkg_cc_proto", ], diff --git a/contrib/sip_proxy/filters/network/source/app_exception_impl.cc b/contrib/sip_proxy/filters/network/source/app_exception_impl.cc index 3df9a6b52d6b2..2b05e0da6ba68 100644 --- a/contrib/sip_proxy/filters/network/source/app_exception_impl.cc +++ b/contrib/sip_proxy/filters/network/source/app_exception_impl.cc @@ -25,7 +25,7 @@ DirectResponse::ResponseType AppException::encode(MessageMetadata& metadata, // unique number. The salt I am using here is the summation of each // character of the proxy's IP address output << ";tag="; - if (metadata.ep().has_value() && metadata.ep().value().length() > 0) { + if (metadata.ep().has_value() && !metadata.ep().value().empty()) { output << fmt::format("{}-", metadata.ep().value()); } std::time_t t; diff --git a/contrib/sip_proxy/filters/network/source/config.cc b/contrib/sip_proxy/filters/network/source/config.cc index 693c852509d6a..7174acb62d444 100644 --- a/contrib/sip_proxy/filters/network/source/config.cc +++ b/contrib/sip_proxy/filters/network/source/config.cc @@ -94,7 +94,7 @@ ConfigImpl::ConfigImpl( envoy::extensions::filters::network::sip_proxy::v3alpha::SipFilter router; envoy::extensions::filters::network::sip_proxy::router::v3alpha::Router default_router; router.set_name(SipFilters::SipFilterNames::get().ROUTER); - router.mutable_typed_config()->PackFrom(default_router); + std::ignore = router.mutable_typed_config()->PackFrom(default_router); processFilter(router); } else { for (const auto& filter : config.sip_filters()) { diff --git a/contrib/sip_proxy/filters/network/source/conn_manager.cc b/contrib/sip_proxy/filters/network/source/conn_manager.cc index bd907cdcbdf5d..14f77accbb05a 100644 --- a/contrib/sip_proxy/filters/network/source/conn_manager.cc +++ b/contrib/sip_proxy/filters/network/source/conn_manager.cc @@ -1,5 +1,7 @@ #include "contrib/sip_proxy/filters/network/source/conn_manager.h" +#include + #include "envoy/common/exception.h" #include "envoy/event/dispatcher.h" @@ -30,7 +32,7 @@ TrafficRoutingAssistantHandler::TrafficRoutingAssistantHandler( void TrafficRoutingAssistantHandler::updateTrafficRoutingAssistant( const std::string& type, const std::string& key, const std::string& val, - const absl::optional context) { + const std::optional context) { bool should_update_tra = true; @@ -50,7 +52,7 @@ void TrafficRoutingAssistantHandler::updateTrafficRoutingAssistant( } QueryStatus TrafficRoutingAssistantHandler::retrieveTrafficRoutingAssistant( - const std::string& type, const std::string& key, const absl::optional context, + const std::string& type, const std::string& key, const std::optional context, SipFilters::DecoderFilterCallbacks& activetrans, std::string& host) { host = {}; @@ -74,7 +76,7 @@ QueryStatus TrafficRoutingAssistantHandler::retrieveTrafficRoutingAssistant( } void TrafficRoutingAssistantHandler::deleteTrafficRoutingAssistant( - const std::string& type, const std::string& key, const absl::optional context) { + const std::string& type, const std::string& key, const std::optional context) { cache_manager_.erase(type, key); if (traClient()) { traClient()->deleteTrafficRoutingAssistant(type, key, context, Tracing::NullSpan::instance(), diff --git a/contrib/sip_proxy/filters/network/source/conn_manager.h b/contrib/sip_proxy/filters/network/source/conn_manager.h index 6349fdc764fae..a898567c071ca 100644 --- a/contrib/sip_proxy/filters/network/source/conn_manager.h +++ b/contrib/sip_proxy/filters/network/source/conn_manager.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/common/pure.h" #include "envoy/common/random_generator.h" #include "envoy/event/deferred_deletable.h" @@ -69,12 +71,12 @@ class TrafficRoutingAssistantHandler : public TrafficRoutingAssistant::RequestCa virtual void updateTrafficRoutingAssistant(const std::string& type, const std::string& key, const std::string& val, - const absl::optional context); + const std::optional context); virtual QueryStatus retrieveTrafficRoutingAssistant( - const std::string& type, const std::string& key, const absl::optional context, + const std::string& type, const std::string& key, const std::optional context, SipFilters::DecoderFilterCallbacks& activetrans, std::string& host); virtual void deleteTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context); + const std::optional context); virtual void subscribeTrafficRoutingAssistant(const std::string& type); void complete(const TrafficRoutingAssistant::ResponseType& type, const std::string& message_type, const absl::any& resp) override; @@ -337,7 +339,7 @@ class ConnectionManager : public Network::ReadFilter, MessageMetadataSharedPtr metadata_; std::list decoder_filters_; ResponseDecoderPtr response_decoder_; - absl::optional cached_route_; + std::optional cached_route_; std::function filter_action_; diff --git a/contrib/sip_proxy/filters/network/source/decoder.cc b/contrib/sip_proxy/filters/network/source/decoder.cc index f264405c85d0c..9f72ee610a69b 100644 --- a/contrib/sip_proxy/filters/network/source/decoder.cc +++ b/contrib/sip_proxy/filters/network/source/decoder.cc @@ -118,9 +118,14 @@ int Decoder::reassemble(Buffer::Instance& data) { // } char len[10]{}; // temporary storage - remaining_data.copyOut(content_length_start + strlen("Content-Length:"), - content_length_end - content_length_start - strlen("Content-Length:"), - len); + size_t value_len = content_length_end - content_length_start - strlen("Content-Length:"); + // The Content-Length value comes from the untrusted SIP message and may be + // longer than the temporary buffer; cap the copy so it cannot overflow len[] + // and keep room for the trailing NUL that atoi() relies on. + if (value_len > sizeof(len) - 1) { + value_len = sizeof(len) - 1; + } + remaining_data.copyOut(content_length_start + strlen("Content-Length:"), value_len, len); clen = std::atoi(len); diff --git a/contrib/sip_proxy/filters/network/source/decoder.h b/contrib/sip_proxy/filters/network/source/decoder.h index 085ac3847e6ca..bd434cc07808e 100644 --- a/contrib/sip_proxy/filters/network/source/decoder.h +++ b/contrib/sip_proxy/filters/network/source/decoder.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "source/common/buffer/buffer_impl.h" #include "contrib/sip_proxy/filters/network/source/filters/filter.h" @@ -51,7 +53,7 @@ class DecoderStateMachine : public Logger::Loggable { : next_state_(next_state), filter_status_(filter_status) {}; State next_state_; - absl::optional filter_status_; + std::optional filter_status_; }; DecoderStatus transportBegin(); diff --git a/contrib/sip_proxy/filters/network/source/encoder.cc b/contrib/sip_proxy/filters/network/source/encoder.cc index 1bc451d6e4b51..e62f4188dc3e2 100644 --- a/contrib/sip_proxy/filters/network/source/encoder.cc +++ b/contrib/sip_proxy/filters/network/source/encoder.cc @@ -16,7 +16,7 @@ void EncoderImpl::encode(const MessageMetadataSharedPtr& metadata, Buffer::Insta case OperationType::Insert: { std::string value = absl::get(operation.value_).value_; if (value == ";ep=" || value == ",opaque=") { - if (metadata->ep().has_value() && metadata->ep().value().length() > 0) { + if (metadata->ep().has_value() && !metadata->ep().value().empty()) { output += raw_msg.substr(previous_position, operation.position_ - previous_position); previous_position = operation.position_; diff --git a/contrib/sip_proxy/filters/network/source/metadata.h b/contrib/sip_proxy/filters/network/source/metadata.h index 1701200429ffc..adf957dc7378d 100644 --- a/contrib/sip_proxy/filters/network/source/metadata.h +++ b/contrib/sip_proxy/filters/network/source/metadata.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "source/common/common/logger.h" #include "contrib/envoy/extensions/filters/network/sip_proxy/v3alpha/sip_proxy.pb.h" @@ -88,7 +90,7 @@ class AffinityEntry { /** * MessageMetadata encapsulates metadata about Sip messages. The various fields are considered - * optional. Unless otherwise noted, accessor methods throw absl::bad_optional_access if the + * optional. Unless otherwise noted, accessor methods throw std::bad_optional_access if the * corresponding value has not been set. */ class MessageMetadata : public Logger::Loggable { @@ -107,10 +109,10 @@ class MessageMetadata : public Logger::Loggable { MethodType methodType() { return method_type_; } void setMethodType(MethodType data) { method_type_ = data; } - absl::optional ep() { return ep_; } + std::optional ep() { return ep_; } void setEP(absl::string_view data) { ep_ = data; } - absl::optional opaque() { return opaque_; } + std::optional opaque() { return opaque_; } void setOpaque(absl::string_view data) { opaque_ = data; } std::vector& operationList() { return operation_list_; } @@ -118,11 +120,11 @@ class MessageMetadata : public Logger::Loggable { #if 1 // TODO Only used for NOKIA customized affinity. should be deleted later. - absl::optional> pCookieIpMap() { return p_cookie_ip_map_; } + std::optional> pCookieIpMap() { return p_cookie_ip_map_; } void setPCookieIpMap(std::pair&& data) { p_cookie_ip_map_ = data; } #endif - absl::optional transactionId() { return transaction_id_; } + std::optional transactionId() { return transaction_id_; } /** * @param data full SIP header */ @@ -192,23 +194,23 @@ class MessageMetadata : public Logger::Loggable { std::vector> headers_{HeaderType::HeaderMaxNum}; std::vector operation_list_; - absl::optional ep_{}; - absl::optional opaque_{}; + std::optional ep_; + std::optional opaque_; - absl::optional> p_cookie_ip_map_{}; + std::optional> p_cookie_ip_map_; - absl::optional transaction_id_{}; + std::optional transaction_id_; - std::string destination_{}; + std::string destination_; - std::vector affinity_{}; + std::vector affinity_; std::vector::iterator affinity_iteration_{affinity_.begin()}; - std::string raw_msg_{}; + std::string raw_msg_; State state_{State::TransportBegin}; bool stop_load_balance_{}; - TraContextMap tra_context_map_{}; + TraContextMap tra_context_map_; bool isDomainMatched( HeaderType type, diff --git a/contrib/sip_proxy/filters/network/source/router/BUILD b/contrib/sip_proxy/filters/network/source/router/BUILD index 4d560bc6ccce9..8e1a429714a04 100644 --- a/contrib/sip_proxy/filters/network/source/router/BUILD +++ b/contrib/sip_proxy/filters/network/source/router/BUILD @@ -30,7 +30,6 @@ envoy_cc_library( deps = [ "//contrib/sip_proxy/filters/network/source:metadata_lib", "//envoy/router:router_interface", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/contrib/sip_proxy/filters/network/source/router/router_impl.cc b/contrib/sip_proxy/filters/network/source/router/router_impl.cc index c6ec139fe7866..6c03945045e0e 100644 --- a/contrib/sip_proxy/filters/network/source/router/router_impl.cc +++ b/contrib/sip_proxy/filters/network/source/router/router_impl.cc @@ -1,5 +1,7 @@ #include "contrib/sip_proxy/filters/network/source/router/router_impl.h" +#include + #include "envoy/upstream/cluster_manager.h" #include "source/common/common/logger.h" @@ -200,7 +202,7 @@ FilterStatus Router::handleAffinity() { if (metadata->pCookieIpMap().has_value()) { auto [key, val] = metadata->pCookieIpMap().value(); ENVOY_LOG(trace, "update p-cookie-ip-map {}={}", key, val); - callbacks_->traHandler()->updateTrafficRoutingAssistant("lskpmc", key, val, absl::nullopt); + callbacks_->traHandler()->updateTrafficRoutingAssistant("lskpmc", key, val, std::nullopt); } const std::shared_ptr options = @@ -724,8 +726,7 @@ FilterStatus ResponseDecoder::transportBegin(MessageMetadataSharedPtr metadata) if (metadata->pCookieIpMap().has_value()) { auto [key, val] = metadata->pCookieIpMap().value(); ENVOY_LOG(trace, "update p-cookie-ip-map {}={}", key, val); - active_trans->traHandler()->updateTrafficRoutingAssistant("lskpmc", key, val, - absl::nullopt); + active_trans->traHandler()->updateTrafficRoutingAssistant("lskpmc", key, val, std::nullopt); } active_trans->startUpstreamResponse(); diff --git a/contrib/sip_proxy/filters/network/source/router/router_impl.h b/contrib/sip_proxy/filters/network/source/router/router_impl.h index eed22812d89cf..40d4758c0b5d6 100644 --- a/contrib/sip_proxy/filters/network/source/router/router_impl.h +++ b/contrib/sip_proxy/filters/network/source/router/router_impl.h @@ -120,8 +120,8 @@ struct ThreadLocalTransactionInfo : public ThreadLocal::ThreadLocalObject, audit_timer_ = dispatcher.createTimer([this]() -> void { auditTimerAction(); }); audit_timer_->enableTimer(std::chrono::seconds(2)); } - absl::flat_hash_map> transaction_info_map_{}; - absl::flat_hash_map> upstream_request_map_{}; + absl::flat_hash_map> transaction_info_map_; + absl::flat_hash_map> upstream_request_map_; std::shared_ptr parent_; Event::Dispatcher& dispatcher_; @@ -284,15 +284,15 @@ class Router : public Upstream::LoadBalancerContextBase, Upstream::ClusterManager& cluster_manager_; RouterStats& stats_; - RouteConstSharedPtr route_{}; + RouteConstSharedPtr route_; const RouteEntry* route_entry_{}; - MessageMetadataSharedPtr metadata_{}; + MessageMetadataSharedPtr metadata_; std::shared_ptr upstream_request_; SipFilters::DecoderFilterCallbacks* callbacks_{}; Upstream::ClusterInfoConstSharedPtr cluster_; Upstream::ThreadLocalCluster* thread_local_cluster_; - std::shared_ptr transaction_infos_{}; + std::shared_ptr transaction_infos_; std::shared_ptr settings_; Server::Configuration::FactoryContext& context_; }; diff --git a/contrib/sip_proxy/filters/network/source/tra/BUILD b/contrib/sip_proxy/filters/network/source/tra/BUILD index 683c74f0be005..183e6b192a989 100644 --- a/contrib/sip_proxy/filters/network/source/tra/BUILD +++ b/contrib/sip_proxy/filters/network/source/tra/BUILD @@ -39,6 +39,5 @@ envoy_cc_library( "//envoy/tracing:tracer_interface", "//source/common/stats:symbol_table_lib", "@abseil-cpp//absl/types:any", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/contrib/sip_proxy/filters/network/source/tra/tra.h b/contrib/sip_proxy/filters/network/source/tra/tra.h index f62f8d98a0055..fd4580502eb83 100644 --- a/contrib/sip_proxy/filters/network/source/tra/tra.h +++ b/contrib/sip_proxy/filters/network/source/tra/tra.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/common/pure.h" #include "envoy/singleton/manager.h" #include "envoy/stream_info/stream_info.h" @@ -7,7 +9,6 @@ #include "absl/container/flat_hash_map.h" #include "absl/types/any.h" -#include "absl/types/optional.h" namespace Envoy { namespace Extensions { @@ -47,18 +48,18 @@ class Client { virtual void setRequestCallbacks(RequestCallbacks& callbacks) PURE; virtual void createTrafficRoutingAssistant( const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, Tracing::Span& parent_span, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) PURE; virtual void updateTrafficRoutingAssistant( const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, Tracing::Span& parent_span, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) PURE; virtual void retrieveTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) PURE; virtual void deleteTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) PURE; virtual void subscribeTrafficRoutingAssistant(const std::string& type, Tracing::Span& parent_span, diff --git a/contrib/sip_proxy/filters/network/source/tra/tra_impl.cc b/contrib/sip_proxy/filters/network/source/tra/tra_impl.cc index e9c0eca313f4e..f05f0f92ba964 100644 --- a/contrib/sip_proxy/filters/network/source/tra/tra_impl.cc +++ b/contrib/sip_proxy/filters/network/source/tra/tra_impl.cc @@ -1,5 +1,7 @@ #include "contrib/sip_proxy/filters/network/source/tra/tra_impl.h" +#include + #include "envoy/config/core/v3/grpc_service.pb.h" #include "envoy/stats/scope.h" @@ -16,7 +18,7 @@ namespace TrafficRoutingAssistant { GrpcClientImpl::GrpcClientImpl(const Grpc::RawAsyncClientSharedPtr& async_client, Event::Dispatcher& dispatcher, - const absl::optional& timeout) + const std::optional& timeout) : async_client_(async_client), dispatcher_(dispatcher), timeout_(timeout) {} GrpcClientImpl::~GrpcClientImpl() { @@ -38,7 +40,7 @@ void GrpcClientImpl::setRequestCallbacks(RequestCallbacks& callbacks) { void GrpcClientImpl::createTrafficRoutingAssistant( const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, Tracing::Span& parent_span, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceRequest request; @@ -60,7 +62,7 @@ void GrpcClientImpl::createTrafficRoutingAssistant( void GrpcClientImpl::updateTrafficRoutingAssistant( const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, Tracing::Span& parent_span, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceRequest request; request.set_type(type); @@ -81,7 +83,7 @@ void GrpcClientImpl::updateTrafficRoutingAssistant( void GrpcClientImpl::retrieveTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) { @@ -100,7 +102,7 @@ void GrpcClientImpl::retrieveTrafficRoutingAssistant(const std::string& type, } void GrpcClientImpl::deleteTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) { diff --git a/contrib/sip_proxy/filters/network/source/tra/tra_impl.h b/contrib/sip_proxy/filters/network/source/tra/tra_impl.h index 14a426bf4e7bd..30d4de7cd62cf 100644 --- a/contrib/sip_proxy/filters/network/source/tra/tra_impl.h +++ b/contrib/sip_proxy/filters/network/source/tra/tra_impl.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/config/core/v3/grpc_service.pb.h" #include "envoy/grpc/async_client.h" #include "envoy/grpc/async_client_manager.h" @@ -37,27 +39,27 @@ class GrpcClientImpl : public Client, public Logger::Loggable { public: GrpcClientImpl(const Grpc::RawAsyncClientSharedPtr& async_client, Event::Dispatcher& dispatcher, - const absl::optional& timeout); + const std::optional& timeout); ~GrpcClientImpl() override; // Extensions::NetworkFilters::SipProxy::TrafficRoutingAssistant::Client void setRequestCallbacks(RequestCallbacks& callbacks) override; void createTrafficRoutingAssistant(const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) override; void updateTrafficRoutingAssistant(const std::string& type, const absl::flat_hash_map& data, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) override; void retrieveTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) override; void deleteTrafficRoutingAssistant(const std::string& type, const std::string& key, - const absl::optional context, + const std::optional context, Tracing::Span& parent_span, const StreamInfo::StreamInfo& stream_info) override; void subscribeTrafficRoutingAssistant(const std::string& type, Tracing::Span& parent_span, @@ -173,7 +175,7 @@ class GrpcClientImpl : public Client, Grpc::AsyncStream< envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceRequest> - stream_{}; + stream_; GrpcClientImpl& parent_; }; @@ -183,7 +185,7 @@ class GrpcClientImpl : public Client, envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse> async_client_; Event::Dispatcher& dispatcher_; - absl::optional timeout_; + std::optional timeout_; std::list> request_callbacks_; std::list> stream_callbacks_; diff --git a/contrib/sip_proxy/filters/network/test/app_exception_impl_test.cc b/contrib/sip_proxy/filters/network/test/app_exception_impl_test.cc index f99ac5240e30b..7eea3c6defaea 100644 --- a/contrib/sip_proxy/filters/network/test/app_exception_impl_test.cc +++ b/contrib/sip_proxy/filters/network/test/app_exception_impl_test.cc @@ -11,6 +11,7 @@ namespace SipProxy { TEST(AppExceptionImplTest, CopyConstructor) { AppException app_ex(AppExceptionType::InternalError, "msg"); + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) AppException copy(app_ex); EXPECT_EQ(app_ex.type_, copy.type_); diff --git a/contrib/sip_proxy/filters/network/test/cache_manager_test.cc b/contrib/sip_proxy/filters/network/test/cache_manager_test.cc index 61816d4809d80..1f0690dbbd3f3 100644 --- a/contrib/sip_proxy/filters/network/test/cache_manager_test.cc +++ b/contrib/sip_proxy/filters/network/test/cache_manager_test.cc @@ -1,3 +1,5 @@ +#include + #include "contrib/sip_proxy/filters/network/source/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -10,7 +12,7 @@ namespace SipProxy { TEST(CacheManagerTest, FindTypeNotExist) { auto cache_manager = CacheManager(); auto result = cache_manager.at("empty", "empty"); - EXPECT_EQ(absl::nullopt, result); + EXPECT_EQ(std::nullopt, result); EXPECT_EQ(false, cache_manager.contains("empty", "empty")); } @@ -19,7 +21,7 @@ TEST(CacheManagerTest, FindKeyNotExist) { cache_manager.initCache("lskpmc", 12); cache_manager.insertCache("lskpmc", "S3F2", "192.168.0.1"); auto result = cache_manager.at("lskpmc", "fake"); - EXPECT_EQ(absl::nullopt, result); + EXPECT_EQ(std::nullopt, result); EXPECT_EQ(true, cache_manager.contains("lskpmc", "S3F2")); EXPECT_EQ(false, cache_manager.contains("lskpmc", "fake")); } diff --git a/contrib/sip_proxy/filters/network/test/decoder_test.cc b/contrib/sip_proxy/filters/network/test/decoder_test.cc index 32eb2a1f7ae5a..36f65812fee26 100644 --- a/contrib/sip_proxy/filters/network/test/decoder_test.cc +++ b/contrib/sip_proxy/filters/network/test/decoder_test.cc @@ -617,6 +617,33 @@ TEST_F(SipDecoderTest, DecodeNOTIFY) { EXPECT_EQ(0U, store_.counter("test.response").value()); } +TEST_F(SipDecoderTest, DecodeContentLengthLongerThanBuffer) { + initializeFilter(yaml); + + // The Content-Length value is far longer than the temporary buffer the decoder + // copies it into. The copy must stay bounded so an attacker-controlled header + // cannot overflow the stack buffer. The leading whitespace parses to 0, so this + // is a complete message once the copy is capped. + const std::string SIP_LONG_CONTENT_LENGTH = + "ACK sip:User.0000@tas01.defult.svc.cluster.local SIP/2.0\x0d\x0a" + "Call-ID: 1-3193@11.0.0.10\x0d\x0a" + "Via: SIP/2.0/TCP 11.0.0.10:15060;branch=z9hG4bK-3193-1-0\x0d\x0a" + "To: \x0d\x0a" + "From: ;tag=1\x0d\x0a" + "Route: \x0d\x0a" + "CSeq: 2 ACK\x0d\x0a" + "Contact: ;tag=1\x0d\x0a" + "Max-Forwards: 70\x0d\x0a" + "Content-Length: 0000000000000000\x0d\x0a" + "\x0d\x0a"; + buffer_.add(SIP_LONG_CONTENT_LENGTH); + + EXPECT_EQ(filter_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, stats_.request_active_.value()); + EXPECT_EQ(0U, store_.counter("test.response").value()); +} + TEST_F(SipDecoderTest, HeaderTest) { StateNameValues stateNameValues_; EXPECT_EQ("Done", stateNameValues_.name(State::Done)); diff --git a/contrib/sip_proxy/filters/network/test/mocks.cc b/contrib/sip_proxy/filters/network/test/mocks.cc index aa3e32fe42a90..d21db35826ed1 100644 --- a/contrib/sip_proxy/filters/network/test/mocks.cc +++ b/contrib/sip_proxy/filters/network/test/mocks.cc @@ -1,6 +1,7 @@ #include "contrib/sip_proxy/filters/network/test/mocks.h" #include +#include #include "source/common/protobuf/protobuf.h" @@ -99,7 +100,7 @@ MockTrafficRoutingAssistantHandler::MockTrafficRoutingAssistantHandler( : TrafficRoutingAssistantHandler(parent, dispatcher, config, context, stream_info) { ON_CALL(*this, retrieveTrafficRoutingAssistant(_, _, _, _, _)) .WillByDefault( - Invoke([&](const std::string&, const std::string&, const absl::optional, + Invoke([&](const std::string&, const std::string&, const std::optional, SipFilters::DecoderFilterCallbacks&, std::string& host) -> QueryStatus { host = "10.0.0.11"; return QueryStatus::Continue; diff --git a/contrib/sip_proxy/filters/network/test/mocks.h b/contrib/sip_proxy/filters/network/test/mocks.h index 6e5b19bef3e2e..ee3621dba1a97 100644 --- a/contrib/sip_proxy/filters/network/test/mocks.h +++ b/contrib/sip_proxy/filters/network/test/mocks.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/router/router.h" @@ -201,14 +202,14 @@ class MockTrafficRoutingAssistantHandler : public TrafficRoutingAssistantHandler Server::Configuration::FactoryContext& context, StreamInfo::StreamInfoImpl& stream_info); MOCK_METHOD(void, updateTrafficRoutingAssistant, (const std::string&, const std::string&, const std::string&, - const absl::optional), + const std::optional), ()); MOCK_METHOD(QueryStatus, retrieveTrafficRoutingAssistant, - (const std::string&, const std::string&, const absl::optional, + (const std::string&, const std::string&, const std::optional, SipFilters::DecoderFilterCallbacks&, std::string&), ()); MOCK_METHOD(void, deleteTrafficRoutingAssistant, - (const std::string&, const std::string&, const absl::optional), ()); + (const std::string&, const std::string&, const std::optional), ()); MOCK_METHOD(void, subscribeTrafficRoutingAssistant, (const std::string&), ()); MOCK_METHOD(void, doSubscribe, (const envoy::extensions::filters::network::sip_proxy::v3alpha::CustomizedAffinity), diff --git a/contrib/sip_proxy/filters/network/test/router_test.cc b/contrib/sip_proxy/filters/network/test/router_test.cc index be8dfb4b230d3..664e4756876b5 100644 --- a/contrib/sip_proxy/filters/network/test/router_test.cc +++ b/contrib/sip_proxy/filters/network/test/router_test.cc @@ -1,4 +1,5 @@ #include +#include #include #include "envoy/tcp/conn_pool.h" @@ -239,7 +240,7 @@ class SipRouterTest : public testing::Test { EXPECT_CALL(*tra_handler_, retrieveTrafficRoutingAssistant(_, _, _, _, _)) .WillRepeatedly( - Invoke([&](const std::string&, const std::string&, const absl::optional, + Invoke([&](const std::string&, const std::string&, const std::optional, SipFilters::DecoderFilterCallbacks&, std::string& host) -> QueryStatus { host = "10.0.0.11"; return QueryStatus::Pending; @@ -408,7 +409,7 @@ TEST_F(SipRouterTest, NoTcpConnPool) { initializeMetadata(MsgType::Request); EXPECT_CALL(context_.server_factory_context_.cluster_manager_.thread_local_cluster_, tcpConnPool(_, _)) - .WillOnce(Return(absl::nullopt)); + .WillOnce(Return(std::nullopt)); try { startRequest(FilterStatus::Continue); } catch (const AppException& ex) { @@ -430,7 +431,7 @@ TEST_F(SipRouterTest, NoTcpConnPoolEmptyDest) { EXPECT_CALL(context_.server_factory_context_.cluster_manager_.thread_local_cluster_, tcpConnPool(_, _)) - .WillOnce(Return(absl::nullopt)); + .WillOnce(Return(std::nullopt)); try { startRequest(FilterStatus::Continue); } catch (const AppException& ex) { @@ -451,7 +452,7 @@ TEST_F(SipRouterTest, QueryPending) { metadata_->resetAffinityIteration(); EXPECT_CALL(*tra_handler_, retrieveTrafficRoutingAssistant(_, _, _, _, _)) .WillRepeatedly( - Invoke([&](const std::string&, const std::string&, const absl::optional, + Invoke([&](const std::string&, const std::string&, const std::optional, SipFilters::DecoderFilterCallbacks&, std::string& host) -> QueryStatus { host = "10.0.0.11"; return QueryStatus::Pending; @@ -469,7 +470,7 @@ TEST_F(SipRouterTest, QueryStop) { metadata_->resetAffinityIteration(); EXPECT_CALL(*tra_handler_, retrieveTrafficRoutingAssistant(_, _, _, _, _)) .WillRepeatedly( - Invoke([&](const std::string&, const std::string&, const absl::optional, + Invoke([&](const std::string&, const std::string&, const std::optional, SipFilters::DecoderFilterCallbacks&, std::string& host) -> QueryStatus { host = ""; return QueryStatus::Stop; diff --git a/contrib/sip_proxy/filters/network/test/tra_test.cc b/contrib/sip_proxy/filters/network/test/tra_test.cc index 6f7a193d657db..297252fd41046 100644 --- a/contrib/sip_proxy/filters/network/test/tra_test.cc +++ b/contrib/sip_proxy/filters/network/test/tra_test.cc @@ -1,5 +1,6 @@ #include #include +#include #include "source/common/buffer/buffer_impl.h" @@ -83,7 +84,7 @@ class SipTraTest : public testing::Test { TEST_F(SipTraTest, TraUpdate) { auto tra_handler = initTraHandler(); - tra_handler->updateTrafficRoutingAssistant("lskpmc", "S2F1", "10.0.0.1", absl::nullopt); + tra_handler->updateTrafficRoutingAssistant("lskpmc", "S2F1", "10.0.0.1", std::nullopt); } TEST_F(SipTraTest, TraUpdateWithSIPContext) { @@ -96,12 +97,12 @@ TEST_F(SipTraTest, TraUpdateWithSIPContext) { TEST_F(SipTraTest, TraRetrieveContinue) { auto tra_handler = initTraHandler(); - tra_handler->updateTrafficRoutingAssistant("lskpmc", "S1F1", "10.0.0.1", absl::nullopt); + tra_handler->updateTrafficRoutingAssistant("lskpmc", "S1F1", "10.0.0.1", std::nullopt); NiceMock callbacks; std::string host = ""; EXPECT_EQ(QueryStatus::Continue, tra_handler->retrieveTrafficRoutingAssistant( - "lskpmc", "S1F1", absl::nullopt, callbacks, host)); + "lskpmc", "S1F1", std::nullopt, callbacks, host)); EXPECT_EQ(host, "10.0.0.1"); } @@ -195,7 +196,7 @@ TEST_F(SipTraTest, TraDoSubscribe) { TEST_F(SipTraTest, TraDelete) { auto tra_handler = initTraHandler(); - tra_handler->deleteTrafficRoutingAssistant("lskpmc", "S1F1", absl::nullopt); + tra_handler->deleteTrafficRoutingAssistant("lskpmc", "S1F1", std::nullopt); } TEST_F(SipTraTest, TraDeleteWithSIPContext) { @@ -228,7 +229,7 @@ TEST_F(SipTraTest, GrpcClientOnSuccessRetrieveRsp) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse>( service_response_config); - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onSuccess(std::move(response), span_); @@ -252,7 +253,7 @@ TEST_F(SipTraTest, GrpcClientOnSuccessCreateRsp) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse>( service_response_config); - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onSuccess(std::move(response), span_); @@ -275,7 +276,7 @@ TEST_F(SipTraTest, GrpcClientOnSuccessUpdateRsp) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse>( service_response_config); - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onSuccess(std::move(response), span_); @@ -298,7 +299,7 @@ TEST_F(SipTraTest, GrpcClientOnSuccessDeleteRsp) { envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse>( service_response_config); - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onSuccess(std::move(response), span_); @@ -309,7 +310,7 @@ TEST_F(SipTraTest, GrpcClientOnReceiveMessage) { response = std::make_unique< envoy::extensions::filters::network::sip_proxy::tra::v3alpha::TraServiceResponse>(); - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onReceiveMessage(std::move(response)); @@ -318,7 +319,7 @@ TEST_F(SipTraTest, GrpcClientOnReceiveMessage) { TEST_F(SipTraTest, GrpcClientOnFailure) { Grpc::Status::GrpcStatus status = Grpc::Status::WellKnownGrpcStatus::Unknown; - auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, absl::nullopt); + auto grpc_client = TrafficRoutingAssistant::GrpcClientImpl(nullptr, dispatcher_, std::nullopt); NiceMock request_callbacks; grpc_client.setRequestCallbacks(request_callbacks); grpc_client.onFailure(status, "", span_); @@ -353,7 +354,7 @@ TEST_F(SipTraTest, Misc) { absl::flat_hash_map data; data.emplace(std::make_pair("S1F1", "10.0.0.1")); - grpc_client.createTrafficRoutingAssistant("lskpmc", data, absl::nullopt, span_, stream_info_); + grpc_client.createTrafficRoutingAssistant("lskpmc", data, std::nullopt, span_, stream_info_); Http::TestRequestHeaderMapImpl request_headers; request_cb->onCreateInitialMetadata(request_headers); diff --git a/contrib/stat_sinks/wasm_filter/source/BUILD b/contrib/stat_sinks/wasm_filter/source/BUILD new file mode 100644 index 0000000000000..99fa56c27e667 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/BUILD @@ -0,0 +1,41 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_contrib_extension", + "envoy_cc_library", + "envoy_contrib_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_library( + name = "wasm_filter_stat_sink_lib", + srcs = ["wasm_filter_stat_sink_impl.cc"], + hdrs = [ + "enriched_metric.h", + "wasm_filter_stat_sink_impl.h", + ], + deps = [ + "//envoy/stats:stats_interface", + "//source/common/common:logger_lib", + "//source/extensions/common/wasm:wasm_lib", + ], + alwayslink = 1, +) + +envoy_cc_contrib_extension( + name = "config_lib", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":wasm_filter_stat_sink_lib", + "//envoy/registry", + "//envoy/server:factory_context_interface", + "//source/common/config:utility_lib", + "//source/extensions/common/wasm:remote_async_datasource_lib", + "//source/extensions/common/wasm:wasm_lib", + "//source/server:configuration_lib", + "@envoy_api//contrib/envoy/extensions/stat_sinks/wasm_filter/v3:pkg_cc_proto", + ], +) diff --git a/contrib/stat_sinks/wasm_filter/source/config.cc b/contrib/stat_sinks/wasm_filter/source/config.cc new file mode 100644 index 0000000000000..087aeff1bcca2 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/config.cc @@ -0,0 +1,68 @@ +#include "contrib/stat_sinks/wasm_filter/source/config.h" + +#include + +#include "envoy/registry/registry.h" +#include "envoy/server/factory_context.h" + +#include "source/common/config/utility.h" +#include "source/extensions/common/wasm/wasm.h" + +#include "contrib/envoy/extensions/stat_sinks/wasm_filter/v3/wasm_filter.pb.validate.h" +#include "contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +absl::StatusOr +WasmFilterSinkFactory::createStatsSink(const Protobuf::Message& proto_config, + Server::Configuration::ServerFactoryContext& context) { + const auto& config = MessageUtil::downcastAndValidate< + const envoy::extensions::stat_sinks::wasm_filter::v3::WasmFilterStatsSinkConfig&>( + proto_config, context.messageValidationContext().staticValidationVisitor()); + + // Scope a TagVector for the plugin's onConfigure to write global tags into. + // This avoids shared thread-local state when multiple wasm_filter sinks exist. + Stats::TagVector startup_tags; + setGlobalTags(&startup_tags); + + auto plugin_config = std::make_unique( + config.wasm_config(), context, context.scope(), context.initManager(), + envoy::config::core::v3::TrafficDirection::UNSPECIFIED, true); + + setGlobalTags(nullptr); + + context.api().customStatNamespaces().registerStatNamespace( + Extensions::Common::Wasm::CustomStatNamespace); + + const auto& inner_sink_config = config.inner_sink(); + auto& inner_factory = + Config::Utility::getAndCheckFactory( + inner_sink_config); + ProtobufTypes::MessagePtr inner_message = Config::Utility::translateToFactoryConfig( + inner_sink_config, context.messageValidationContext().staticValidationVisitor(), + inner_factory); + + auto inner_sink = inner_factory.createStatsSink(*inner_message, context); + RETURN_IF_NOT_OK_REF(inner_sink.status()); + + return std::make_unique( + std::move(plugin_config), std::move(inner_sink.value()), context.scope().symbolTable(), + std::move(startup_tags)); +} + +ProtobufTypes::MessagePtr WasmFilterSinkFactory::createEmptyConfigProto() { + return std::make_unique< + envoy::extensions::stat_sinks::wasm_filter::v3::WasmFilterStatsSinkConfig>(); +} + +std::string WasmFilterSinkFactory::name() const { return WasmFilterName; } + +REGISTER_FACTORY(WasmFilterSinkFactory, Server::Configuration::StatsSinkFactory); + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/source/config.h b/contrib/stat_sinks/wasm_filter/source/config.h new file mode 100644 index 0000000000000..d7633a430eac1 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/config.h @@ -0,0 +1,35 @@ +#pragma once + +#include + +#include "envoy/server/factory_context.h" + +#include "source/extensions/common/wasm/remote_async_datasource.h" +#include "source/server/configuration_impl.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +constexpr char WasmFilterName[] = "envoy.stat_sinks.wasm_filter"; + +class WasmFilterSinkFactory : Logger::Loggable, + public Server::Configuration::StatsSinkFactory { +public: + absl::StatusOr + createStatsSink(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& context) override; + + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + + std::string name() const override; + +private: + RemoteAsyncDataProviderPtr remote_data_provider_; +}; + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/source/enriched_metric.h b/contrib/stat_sinks/wasm_filter/source/enriched_metric.h new file mode 100644 index 0000000000000..230c286e0d0ad --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/enriched_metric.h @@ -0,0 +1,303 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/stats/histogram.h" +#include "envoy/stats/stats.h" + +#include "source/common/stats/symbol_table.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +// Merges original tags with extra tags. Extra tags are appended. +inline Stats::TagVector mergeTags(const Stats::TagVector& original, const Stats::TagVector& extra) { + if (extra.empty()) { + return original; + } + Stats::TagVector merged; + merged.reserve(original.size() + extra.size()); + merged.insert(merged.end(), original.begin(), original.end()); + merged.insert(merged.end(), extra.begin(), extra.end()); + return merged; +} + +// Wraps an existing Counter, overriding name() and tags() while forwarding +// everything else. Used to inject global tags and per-metric name overrides. +class EnrichedCounter : public Stats::Counter { +public: + EnrichedCounter(const Stats::Counter& original, const Stats::TagVector& extra_tags, + const std::string& name_override = "") + : original_(original), extra_tags_(extra_tags), name_override_(name_override) { + if (!name_override_.empty()) { + override_stat_name_.emplace(name_override_, + const_cast(original_).symbolTable()); + } + } + + std::string name() const override { + return name_override_.empty() ? original_.name() : name_override_; + } + Stats::StatName statName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() : original_.statName(); + } + Stats::TagVector tags() const override { return mergeTags(original_.tags(), extra_tags_); } + std::string tagExtractedName() const override { + return name_override_.empty() ? original_.tagExtractedName() : name_override_; + } + Stats::StatName tagExtractedStatName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() + : original_.tagExtractedStatName(); + } + void iterateTagStatNames(const Stats::Metric::TagStatNameIterFn& fn) const override { + original_.iterateTagStatNames(fn); + } + bool used() const override { return original_.used(); } + void markUnused() override {} + bool hidden() const override { return original_.hidden(); } + Stats::SymbolTable& symbolTable() override { + return const_cast(original_).symbolTable(); + } + const Stats::SymbolTable& constSymbolTable() const override { + return original_.constSymbolTable(); + } + + void incRefCount() override {} + bool decRefCount() override { return false; } + uint32_t use_count() const override { return 1; } + + void add(uint64_t) override {} + void inc() override {} + uint64_t latch() override { return 0; } + void reset() override {} + uint64_t value() const override { return original_.value(); } + +private: + const Stats::Counter& original_; + const Stats::TagVector& extra_tags_; + const std::string& name_override_; + std::optional override_stat_name_; +}; + +// Wraps an existing Gauge with tag/name overrides. +class EnrichedGauge : public Stats::Gauge { +public: + EnrichedGauge(const Stats::Gauge& original, const Stats::TagVector& extra_tags, + const std::string& name_override = "") + : original_(original), extra_tags_(extra_tags), name_override_(name_override) { + if (!name_override_.empty()) { + override_stat_name_.emplace(name_override_, + const_cast(original_).symbolTable()); + } + } + + std::string name() const override { + return name_override_.empty() ? original_.name() : name_override_; + } + Stats::StatName statName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() : original_.statName(); + } + Stats::TagVector tags() const override { return mergeTags(original_.tags(), extra_tags_); } + std::string tagExtractedName() const override { + return name_override_.empty() ? original_.tagExtractedName() : name_override_; + } + Stats::StatName tagExtractedStatName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() + : original_.tagExtractedStatName(); + } + void iterateTagStatNames(const Stats::Metric::TagStatNameIterFn& fn) const override { + original_.iterateTagStatNames(fn); + } + bool used() const override { return original_.used(); } + void markUnused() override {} + bool hidden() const override { return original_.hidden(); } + Stats::SymbolTable& symbolTable() override { + return const_cast(original_).symbolTable(); + } + const Stats::SymbolTable& constSymbolTable() const override { + return original_.constSymbolTable(); + } + + void incRefCount() override {} + bool decRefCount() override { return false; } + uint32_t use_count() const override { return 1; } + + void add(uint64_t) override {} + void dec() override {} + void inc() override {} + void set(uint64_t) override {} + void sub(uint64_t) override {} + uint64_t value() const override { return original_.value(); } + void setParentValue(uint64_t) override {} + ImportMode importMode() const override { return original_.importMode(); } + void mergeImportMode(ImportMode) override {} + +private: + const Stats::Gauge& original_; + const Stats::TagVector& extra_tags_; + const std::string& name_override_; + std::optional override_stat_name_; +}; + +// Wraps an existing ParentHistogram with tag/name overrides. +class EnrichedHistogram : public Stats::ParentHistogram { +public: + EnrichedHistogram(const Stats::ParentHistogram& original, const Stats::TagVector& extra_tags, + const std::string& name_override = "") + : original_(original), extra_tags_(extra_tags), name_override_(name_override) { + if (!name_override_.empty()) { + override_stat_name_.emplace(name_override_, + const_cast(original_).symbolTable()); + } + } + + std::string name() const override { + return name_override_.empty() ? original_.name() : name_override_; + } + Stats::StatName statName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() : original_.statName(); + } + Stats::TagVector tags() const override { return mergeTags(original_.tags(), extra_tags_); } + std::string tagExtractedName() const override { + return name_override_.empty() ? original_.tagExtractedName() : name_override_; + } + Stats::StatName tagExtractedStatName() const override { + return override_stat_name_.has_value() ? override_stat_name_->statName() + : original_.tagExtractedStatName(); + } + void iterateTagStatNames(const Stats::Metric::TagStatNameIterFn& fn) const override { + original_.iterateTagStatNames(fn); + } + bool used() const override { return original_.used(); } + void markUnused() override {} + bool hidden() const override { return original_.hidden(); } + Stats::SymbolTable& symbolTable() override { + return const_cast(original_).symbolTable(); + } + const Stats::SymbolTable& constSymbolTable() const override { + return original_.constSymbolTable(); + } + + void incRefCount() override {} + bool decRefCount() override { return false; } + uint32_t use_count() const override { return 1; } + + Histogram::Unit unit() const override { return original_.unit(); } + void recordValue(uint64_t) override {} + + void merge() override {} + const Stats::HistogramStatistics& intervalStatistics() const override { + return original_.intervalStatistics(); + } + const Stats::HistogramStatistics& cumulativeStatistics() const override { + return original_.cumulativeStatistics(); + } + std::string quantileSummary() const override { return original_.quantileSummary(); } + std::string bucketSummary() const override { return original_.bucketSummary(); } + std::vector detailedTotalBuckets() const override { + return original_.detailedTotalBuckets(); + } + std::vector detailedIntervalBuckets() const override { + return original_.detailedIntervalBuckets(); + } + uint64_t cumulativeCountLessThanOrEqualToValue(double value) const override { + return original_.cumulativeCountLessThanOrEqualToValue(value); + } + +private: + const Stats::ParentHistogram& original_; + const Stats::TagVector& extra_tags_; + const std::string& name_override_; + std::optional override_stat_name_; +}; + +// A standalone synthetic counter with stored name, value, and tags. +// Used to inject custom metrics that don't exist in Envoy's stats system. +class SyntheticCounter : public Stats::Counter { +public: + SyntheticCounter(Stats::SymbolTable& symbol_table, const std::string& name, uint64_t value, + Stats::TagVector tags) + : symbol_table_(symbol_table), name_(name), value_(value), tags_(std::move(tags)), + stat_name_storage_(name, symbol_table) {} + + std::string name() const override { return name_; } + Stats::StatName statName() const override { return stat_name_storage_.statName(); } + Stats::TagVector tags() const override { return tags_; } + std::string tagExtractedName() const override { return name_; } + Stats::StatName tagExtractedStatName() const override { return stat_name_storage_.statName(); } + void iterateTagStatNames(const Stats::Metric::TagStatNameIterFn&) const override {} + bool used() const override { return true; } + void markUnused() override {} + bool hidden() const override { return false; } + Stats::SymbolTable& symbolTable() override { return symbol_table_; } + const Stats::SymbolTable& constSymbolTable() const override { return symbol_table_; } + + void incRefCount() override {} + bool decRefCount() override { return false; } + uint32_t use_count() const override { return 1; } + + void add(uint64_t) override {} + void inc() override {} + uint64_t latch() override { return 0; } + void reset() override {} + uint64_t value() const override { return value_; } + +private: + Stats::SymbolTable& symbol_table_; + std::string name_; + uint64_t value_; + Stats::TagVector tags_; + Stats::StatNameManagedStorage stat_name_storage_; +}; + +// A standalone synthetic gauge with stored name, value, and tags. +class SyntheticGauge : public Stats::Gauge { +public: + SyntheticGauge(Stats::SymbolTable& symbol_table, const std::string& name, uint64_t value, + Stats::TagVector tags) + : symbol_table_(symbol_table), name_(name), value_(value), tags_(std::move(tags)), + stat_name_storage_(name, symbol_table) {} + + std::string name() const override { return name_; } + Stats::StatName statName() const override { return stat_name_storage_.statName(); } + Stats::TagVector tags() const override { return tags_; } + std::string tagExtractedName() const override { return name_; } + Stats::StatName tagExtractedStatName() const override { return stat_name_storage_.statName(); } + void iterateTagStatNames(const Stats::Metric::TagStatNameIterFn&) const override {} + bool used() const override { return true; } + void markUnused() override {} + bool hidden() const override { return false; } + Stats::SymbolTable& symbolTable() override { return symbol_table_; } + const Stats::SymbolTable& constSymbolTable() const override { return symbol_table_; } + + void incRefCount() override {} + bool decRefCount() override { return false; } + uint32_t use_count() const override { return 1; } + + void add(uint64_t) override {} + void dec() override {} + void inc() override {} + void set(uint64_t) override {} + void sub(uint64_t) override {} + uint64_t value() const override { return value_; } + void setParentValue(uint64_t) override {} + ImportMode importMode() const override { return ImportMode::NeverImport; } + void mergeImportMode(ImportMode) override {} + +private: + Stats::SymbolTable& symbol_table_; + std::string name_; + uint64_t value_; + Stats::TagVector tags_; + Stats::StatNameManagedStorage stat_name_storage_; +}; + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.cc b/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.cc new file mode 100644 index 0000000000000..7d40ffe84bfc4 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.cc @@ -0,0 +1,697 @@ +#include "contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h" + +#include +#include +#include + +using proxy_wasm::RegisterForeignFunction; +using proxy_wasm::WasmResult; + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +namespace { + +thread_local StatsFilterContext* active_context = nullptr; +thread_local Stats::TagVector* active_global_tags = nullptr; + +constexpr size_t kU32 = sizeof(uint32_t); +constexpr size_t kU64 = sizeof(uint64_t); + +bool readU32(const char* data, size_t total, size_t& offset, uint32_t& out) { + if (offset + kU32 > total) { + return false; + } + memcpy(&out, data + offset, kU32); // NOLINT(safe-memcpy) + offset += kU32; + return true; +} + +bool readU64(const char* data, size_t total, size_t& offset, uint64_t& out) { + if (offset + kU64 > total) { + return false; + } + memcpy(&out, data + offset, kU64); // NOLINT(safe-memcpy) + offset += kU64; + return true; +} + +bool readStr(const char* data, size_t total, size_t& offset, std::string& out) { + uint32_t len = 0; + if (!readU32(data, total, offset, len)) { + return false; + } + if (offset + len > total) { + return false; + } + out.assign(data + offset, len); + offset += len; + return true; +} + +bool readIndexBlock(const char* data, size_t total, size_t& offset, + absl::flat_hash_set& dest) { + uint32_t count = 0; + if (!readU32(data, total, offset, count)) { + return false; + } + if (count > (total - offset) / kU32) { + return false; + } + dest.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + uint32_t idx = 0; + if (!readU32(data, total, offset, idx)) { + return false; + } + dest.insert(idx); + } + return true; +} + +bool readTagVector(const char* data, size_t total, size_t& offset, Stats::TagVector& tags) { + uint32_t tag_count = 0; + if (!readU32(data, total, offset, tag_count)) { + return false; + } + if (tag_count > (total - offset) / (2 * kU32)) { + return false; + } + tags.reserve(tag_count); + for (uint32_t t = 0; t < tag_count; ++t) { + Stats::Tag tag; + if (!readStr(data, total, offset, tag.name_) || !readStr(data, total, offset, tag.value_)) { + return false; + } + tags.push_back(std::move(tag)); + } + return true; +} + +void writeU32(char* out, size_t& pos, uint32_t v) { + memcpy(out + pos, &v, kU32); // NOLINT(safe-memcpy) + pos += kU32; +} + +void writeStr(char* out, size_t& pos, const std::string& s) { + writeU32(out, pos, static_cast(s.size())); + if (!s.empty()) { + memcpy(out + pos, s.data(), s.size()); // NOLINT(safe-memcpy) + pos += s.size(); + } +} + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_emit +// +// Wire format (packed uint32_t array): +// [counter_count] [counter_idx_0] ... [counter_idx_N] +// [gauge_count] [gauge_idx_0] ... [gauge_idx_M] +// [hist_count] [hist_idx_0] ... [hist_idx_K] (optional) +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterEmit( + "stats_filter_emit", + [](proxy_wasm::WasmBase&, std::string_view arguments, + const std::function& /*alloc_result*/) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr) { + return WasmResult::InternalFailure; + } + + const char* data = arguments.data(); + const size_t total = arguments.size(); + + if (total < kU32) { + return WasmResult::BadArgument; + } + + size_t offset = 0; + if (!readIndexBlock(data, total, offset, ctx->kept_counter_indices)) { + return WasmResult::BadArgument; + } + if (!readIndexBlock(data, total, offset, ctx->kept_gauge_indices)) { + return WasmResult::BadArgument; + } + if (offset < total) { + if (!readIndexBlock(data, total, offset, ctx->kept_histogram_indices)) { + return WasmResult::BadArgument; + } + ctx->histogram_block_present = true; + } + + ctx->emit_called = true; + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_set_global_tags +// +// Called once during plugin startup (onConfigure) to set tags that will be +// applied to ALL metrics on every flush. +// +// Wire format: +// [tag_count: u32] +// For each tag: +// [name_len: u32] [name bytes] [value_len: u32] [value bytes] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterSetGlobalTags( + "stats_filter_set_global_tags", + [](proxy_wasm::WasmBase&, std::string_view arguments, + const std::function& /*alloc_result*/) -> WasmResult { + const char* data = arguments.data(); + const size_t total = arguments.size(); + size_t offset = 0; + + Stats::TagVector* tags = active_global_tags; + if (tags == nullptr) { + return WasmResult::InternalFailure; + } + tags->clear(); + if (!readTagVector(data, total, offset, *tags)) { + return WasmResult::BadArgument; + } + + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_set_name_overrides +// +// Called per flush to rename specific metrics. +// +// Wire format: +// [count: u32] +// For each override: +// [type: u32 (1=counter, 2=gauge, 3=histogram)] +// [index: u32] +// [new_name_len: u32] [new_name bytes] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterSetNameOverrides( + "stats_filter_set_name_overrides", + [](proxy_wasm::WasmBase&, std::string_view arguments, + const std::function& /*alloc_result*/) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr) { + return WasmResult::InternalFailure; + } + + const char* data = arguments.data(); + const size_t total = arguments.size(); + size_t offset = 0; + + uint32_t count = 0; + if (!readU32(data, total, offset, count)) { + return WasmResult::BadArgument; + } + if (count > (total - offset) / (3 * kU32)) { + return WasmResult::BadArgument; + } + + ctx->name_overrides.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + NameOverride ovr; + if (!readU32(data, total, offset, ovr.type) || !readU32(data, total, offset, ovr.index)) { + return WasmResult::BadArgument; + } + if (!readStr(data, total, offset, ovr.new_name)) { + return WasmResult::BadArgument; + } + ctx->name_overrides.push_back(std::move(ovr)); + } + + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_inject_metrics +// +// Called per flush to inject synthetic counters and gauges. +// +// Wire format: +// [counter_count: u32] +// For each counter: +// [name_len: u32] [name bytes] [value: u64] +// [tag_count: u32] for each tag: [name_len] [name] [value_len] [value] +// [gauge_count: u32] +// For each gauge: +// [name_len: u32] [name bytes] [value: u64] +// [tag_count: u32] for each tag: [name_len] [name] [value_len] [value] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterInjectMetrics( + "stats_filter_inject_metrics", + [](proxy_wasm::WasmBase&, std::string_view arguments, + const std::function& /*alloc_result*/) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr) { + return WasmResult::InternalFailure; + } + + const char* data = arguments.data(); + const size_t total = arguments.size(); + size_t offset = 0; + + auto readMetricBlock = [&](std::vector& dest) -> bool { + uint32_t count = 0; + if (!readU32(data, total, offset, count)) { + return false; + } + if (count > (total - offset) / (2 * kU32 + kU64)) { + return false; + } + dest.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + SyntheticMetricDef def; + if (!readStr(data, total, offset, def.name)) { + return false; + } + if (!readU64(data, total, offset, def.value)) { + return false; + } + if (!readTagVector(data, total, offset, def.tags)) { + return false; + } + dest.push_back(std::move(def)); + } + return true; + }; + + if (!readMetricBlock(ctx->synthetic_counters)) { + return WasmResult::BadArgument; + } + if (!readMetricBlock(ctx->synthetic_gauges)) { + return WasmResult::BadArgument; + } + + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_get_metric_tags +// +// Input: [type: u32] [index: u32] +// Output: [tag_count: u32] for each: [name_len][name][value_len][value] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterGetMetricTags( + "stats_filter_get_metric_tags", + [](proxy_wasm::WasmBase&, std::string_view arguments, + const std::function& alloc_result) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr || ctx->snapshot == nullptr) { + return WasmResult::InternalFailure; + } + + if (arguments.size() < 2 * kU32) { + return WasmResult::BadArgument; + } + + size_t offset = 0; + uint32_t type = 0; + uint32_t index = 0; + readU32(arguments.data(), arguments.size(), offset, type); + readU32(arguments.data(), arguments.size(), offset, index); + + Stats::TagVector tags; + if (type == 1) { + // Translate buffer-order index to snapshot-order index. + if (index >= ctx->counter_buffer_to_snapshot.size()) { + return WasmResult::BadArgument; + } + uint32_t snap_idx = ctx->counter_buffer_to_snapshot[index]; + const auto& counters = ctx->snapshot->counters(); + if (snap_idx >= counters.size()) { + return WasmResult::BadArgument; + } + tags = counters[snap_idx].counter_.get().tags(); + } else if (type == 2) { + if (index >= ctx->gauge_buffer_to_snapshot.size()) { + return WasmResult::BadArgument; + } + uint32_t snap_idx = ctx->gauge_buffer_to_snapshot[index]; + const auto& gauges = ctx->snapshot->gauges(); + if (snap_idx >= gauges.size()) { + return WasmResult::BadArgument; + } + tags = gauges[snap_idx].get().tags(); + } else if (type == 3) { + const auto& histograms = ctx->snapshot->histograms(); + if (index >= histograms.size()) { + return WasmResult::BadArgument; + } + tags = histograms[index].get().tags(); + } else { + return WasmResult::BadArgument; + } + + size_t out_size = kU32; + for (const auto& tag : tags) { + out_size += kU32 + tag.name_.size() + kU32 + tag.value_.size(); + } + + auto* out = static_cast(alloc_result(out_size)); + size_t pos = 0; + writeU32(out, pos, static_cast(tags.size())); + for (const auto& tag : tags) { + writeStr(out, pos, tag.name_); + writeStr(out, pos, tag.value_); + } + + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_get_all_metric_tags +// +// Bulk: returns tags for used counters and gauges (in buffer order) and all histograms. +// Output: 3 blocks (counters, gauges, histograms), each: +// [metric_count: u32] +// For each metric: [tag_count: u32] for each tag: [name_len][name][value_len][value] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterGetAllMetricTags( + "stats_filter_get_all_metric_tags", + [](proxy_wasm::WasmBase&, std::string_view /*arguments*/, + const std::function& alloc_result) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr || ctx->snapshot == nullptr) { + return WasmResult::InternalFailure; + } + + const auto& counters = ctx->snapshot->counters(); + const auto& gauges = ctx->snapshot->gauges(); + const auto& histograms = ctx->snapshot->histograms(); + + auto collectBufferedTags = [](const auto& metrics, const auto& buffer_to_snapshot, + auto extract_tags) { + std::vector all; + all.reserve(buffer_to_snapshot.size()); + for (uint32_t snapshot_index : buffer_to_snapshot) { + all.push_back(extract_tags(metrics[snapshot_index])); + } + return all; + }; + + auto counter_tags = + collectBufferedTags(counters, ctx->counter_buffer_to_snapshot, + [](const auto& c) { return c.counter_.get().tags(); }); + auto gauge_tags = collectBufferedTags(gauges, ctx->gauge_buffer_to_snapshot, + [](const auto& g) { return g.get().tags(); }); + std::vector histogram_tags; + histogram_tags.reserve(histograms.size()); + for (const auto& h : histograms) { + histogram_tags.push_back(h.get().tags()); + } + + auto blockSize = [](const std::vector& tag_groups) -> size_t { + size_t sz = kU32; + for (const auto& tags : tag_groups) { + sz += kU32; + for (const auto& tag : tags) { + sz += kU32 + tag.name_.size() + kU32 + tag.value_.size(); + } + } + return sz; + }; + + size_t out_size = blockSize(counter_tags) + blockSize(gauge_tags) + blockSize(histogram_tags); + auto* out = static_cast(alloc_result(out_size)); + size_t pos = 0; + + auto writeBlock = [&](const std::vector& tag_groups) { + writeU32(out, pos, static_cast(tag_groups.size())); + for (const auto& tags : tag_groups) { + writeU32(out, pos, static_cast(tags.size())); + for (const auto& tag : tags) { + writeStr(out, pos, tag.name_); + writeStr(out, pos, tag.value_); + } + } + }; + + writeBlock(counter_tags); + writeBlock(gauge_tags); + writeBlock(histogram_tags); + + return WasmResult::Ok; + }); + +// --------------------------------------------------------------------------- +// Foreign function: stats_filter_get_histograms +// +// Returns histogram names so the plugin can filter them. +// Output: [count: u32] for each: [name_len: u32] [name bytes] +// --------------------------------------------------------------------------- +RegisterForeignFunction registerStatsFilterGetHistograms( + "stats_filter_get_histograms", + [](proxy_wasm::WasmBase&, std::string_view /*arguments*/, + const std::function& alloc_result) -> WasmResult { + auto* ctx = active_context; + if (ctx == nullptr || ctx->snapshot == nullptr) { + return WasmResult::InternalFailure; + } + + const auto& histograms = ctx->snapshot->histograms(); + + size_t out_size = kU32; + for (const auto& hist_ref : histograms) { + out_size += kU32 + hist_ref.get().name().size(); + } + + auto* out = static_cast(alloc_result(out_size)); + size_t pos = 0; + + writeU32(out, pos, static_cast(histograms.size())); + for (const auto& hist_ref : histograms) { + writeStr(out, pos, hist_ref.get().name()); + } + + return WasmResult::Ok; + }); + +} // namespace + +StatsFilterContext* getActiveContext() { return active_context; } +void setActiveContext(StatsFilterContext* ctx) { active_context = ctx; } + +Stats::TagVector* getGlobalTags() { return active_global_tags; } +void setGlobalTags(Stats::TagVector* tags) { active_global_tags = tags; } + +void buildBufferToSnapshotMaps(Stats::MetricSnapshot& snapshot, StatsFilterContext& ctx) { + const auto& counters = snapshot.counters(); + ctx.counter_buffer_to_snapshot.reserve(counters.size()); + for (uint32_t i = 0; i < counters.size(); ++i) { + if (counters[i].counter_.get().used()) { + ctx.counter_buffer_to_snapshot.push_back(i); + } + } + + const auto& gauges = snapshot.gauges(); + ctx.gauge_buffer_to_snapshot.reserve(gauges.size()); + for (uint32_t i = 0; i < gauges.size(); ++i) { + if (gauges[i].get().used()) { + ctx.gauge_buffer_to_snapshot.push_back(i); + } + } +} + +void processFilterDecisionsAndFlush(Stats::MetricSnapshot& snapshot, StatsFilterContext& context, + Stats::TagVector& global_tags, Stats::SymbolTable& symbol_table, + Stats::Sink& inner_sink) { + auto translateIndices = [](absl::flat_hash_set& indices, + const std::vector& mapping) { + absl::flat_hash_set translated; + translated.reserve(indices.size()); + for (uint32_t buf_idx : indices) { + if (buf_idx < mapping.size()) { + translated.insert(mapping[buf_idx]); + } + } + indices = std::move(translated); + }; + + translateIndices(context.kept_counter_indices, context.counter_buffer_to_snapshot); + translateIndices(context.kept_gauge_indices, context.gauge_buffer_to_snapshot); + + for (auto& ovr : context.name_overrides) { + if (ovr.type == 1 && ovr.index < context.counter_buffer_to_snapshot.size()) { + ovr.index = context.counter_buffer_to_snapshot[ovr.index]; + } else if (ovr.type == 2 && ovr.index < context.gauge_buffer_to_snapshot.size()) { + ovr.index = context.gauge_buffer_to_snapshot[ovr.index]; + } + } + + const auto& counters = snapshot.counters(); + const auto& gauges = snapshot.gauges(); + + bool has_enrichments = !global_tags.empty() || !context.name_overrides.empty() || + !context.synthetic_counters.empty() || !context.synthetic_gauges.empty(); + + if (!context.emit_called && !has_enrichments) { + inner_sink.flush(snapshot); + return; + } + + if (!context.emit_called && has_enrichments) { + // Plugin didn't filter, but has enrichments - keep all metrics. + for (uint32_t i = 0; i < counters.size(); ++i) { + context.kept_counter_indices.insert(i); + } + for (uint32_t i = 0; i < gauges.size(); ++i) { + context.kept_gauge_indices.insert(i); + } + } + + if (context.emit_called) { + // Unused metrics always pass through regardless of filtering. + for (uint32_t i = 0; i < counters.size(); ++i) { + if (!counters[i].counter_.get().used()) { + context.kept_counter_indices.insert(i); + } + } + for (uint32_t i = 0; i < gauges.size(); ++i) { + if (!gauges[i].get().used()) { + context.kept_gauge_indices.insert(i); + } + } + } + + EnrichedMetricSnapshot enriched(snapshot, context, global_tags, symbol_table); + inner_sink.flush(enriched); +} + +// --------------------------------------------------------------------------- +// EnrichedMetricSnapshot +// --------------------------------------------------------------------------- + +EnrichedMetricSnapshot::EnrichedMetricSnapshot(Stats::MetricSnapshot& original, + const StatsFilterContext& ctx, + const Stats::TagVector& global_tags, + Stats::SymbolTable& symbol_table) + : original_(original), symbol_table_(symbol_table) { + const auto& src_counters = original.counters(); + const auto& src_gauges = original.gauges(); + const auto& src_histograms = original.histograms(); + + // Build per-type name override lookup. + counter_name_overrides_.resize(src_counters.size()); + gauge_name_overrides_.resize(src_gauges.size()); + histogram_name_overrides_.resize(src_histograms.size()); + for (const auto& ovr : ctx.name_overrides) { + if (ovr.type == 1 && ovr.index < counter_name_overrides_.size()) { + counter_name_overrides_[ovr.index] = ovr.new_name; + } else if (ovr.type == 2 && ovr.index < gauge_name_overrides_.size()) { + gauge_name_overrides_[ovr.index] = ovr.new_name; + } else if (ovr.type == 3 && ovr.index < histogram_name_overrides_.size()) { + histogram_name_overrides_[ovr.index] = ovr.new_name; + } + } + + // Build enriched counters. + counter_wrappers_.reserve(ctx.kept_counter_indices.size()); + enriched_counters_.reserve(ctx.kept_counter_indices.size()); + for (uint32_t i = 0; i < src_counters.size(); ++i) { + if (ctx.kept_counter_indices.contains(i)) { + counter_wrappers_.emplace_back(src_counters[i].counter_.get(), global_tags, + counter_name_overrides_[i]); + enriched_counters_.push_back({src_counters[i].delta_, counter_wrappers_.back()}); + } + } + + // Build enriched gauges. + gauge_wrappers_.reserve(ctx.kept_gauge_indices.size()); + enriched_gauges_.reserve(ctx.kept_gauge_indices.size()); + for (uint32_t i = 0; i < src_gauges.size(); ++i) { + if (ctx.kept_gauge_indices.contains(i)) { + gauge_wrappers_.emplace_back(src_gauges[i].get(), global_tags, gauge_name_overrides_[i]); + enriched_gauges_.emplace_back(gauge_wrappers_.back()); + } + } + + // Build enriched histograms. + if (!ctx.histogram_block_present) { + // No histogram block in emit call -- pass all through with enrichment. + histogram_wrappers_.reserve(src_histograms.size()); + enriched_histograms_.reserve(src_histograms.size()); + for (uint32_t i = 0; i < src_histograms.size(); ++i) { + histogram_wrappers_.emplace_back( + src_histograms[i].get(), global_tags, + i < histogram_name_overrides_.size() ? histogram_name_overrides_[i] : std::string()); + enriched_histograms_.emplace_back(histogram_wrappers_.back()); + } + } else { + histogram_wrappers_.reserve(ctx.kept_histogram_indices.size()); + enriched_histograms_.reserve(ctx.kept_histogram_indices.size()); + for (uint32_t i = 0; i < src_histograms.size(); ++i) { + if (ctx.kept_histogram_indices.contains(i)) { + histogram_wrappers_.emplace_back(src_histograms[i].get(), global_tags, + histogram_name_overrides_[i]); + enriched_histograms_.emplace_back(histogram_wrappers_.back()); + } + } + } + + // Append synthetic counters. + if (!ctx.synthetic_counters.empty()) { + synthetic_counter_objs_.reserve(ctx.synthetic_counters.size()); + for (const auto& def : ctx.synthetic_counters) { + auto merged_tags = mergeTags(def.tags, global_tags); + synthetic_counter_objs_.emplace_back(symbol_table_, def.name, def.value, + std::move(merged_tags)); + } + for (auto& sc : synthetic_counter_objs_) { + enriched_counters_.push_back({sc.value(), sc}); + } + } + + // Append synthetic gauges. + if (!ctx.synthetic_gauges.empty()) { + synthetic_gauge_objs_.reserve(ctx.synthetic_gauges.size()); + for (const auto& def : ctx.synthetic_gauges) { + auto merged_tags = mergeTags(def.tags, global_tags); + synthetic_gauge_objs_.emplace_back(symbol_table_, def.name, def.value, + std::move(merged_tags)); + } + for (auto& sg : synthetic_gauge_objs_) { + enriched_gauges_.emplace_back(sg); + } + } +} + +// --------------------------------------------------------------------------- +// WasmFilterStatsSink +// --------------------------------------------------------------------------- + +WasmFilterStatsSink::WasmFilterStatsSink(Common::Wasm::PluginConfigPtr plugin_config, + Stats::SinkPtr inner_sink, + Stats::SymbolTable& symbol_table, + Stats::TagVector initial_global_tags) + : plugin_config_(std::move(plugin_config)), inner_sink_(std::move(inner_sink)), + symbol_table_(symbol_table), global_tags_(std::move(initial_global_tags)) {} + +void WasmFilterStatsSink::flush(Stats::MetricSnapshot& snapshot) { + context_.clear(); + context_.snapshot = &snapshot; + + Common::Wasm::Wasm* wasm = plugin_config_->wasm(); + if (wasm == nullptr) { + inner_sink_->flush(snapshot); + return; + } + + buildBufferToSnapshotMaps(snapshot, context_); + + setActiveContext(&context_); + setGlobalTags(&global_tags_); + + wasm->onStatsUpdate(plugin_config_->plugin(), snapshot); + + setActiveContext(nullptr); + setGlobalTags(nullptr); + + processFilterDecisionsAndFlush(snapshot, context_, global_tags_, symbol_table_, *inner_sink_); +} + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h b/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h new file mode 100644 index 0000000000000..3925608decde5 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include + +#include "envoy/stats/sink.h" + +#include "source/extensions/common/wasm/wasm.h" + +#include "absl/container/flat_hash_set.h" +#include "contrib/stat_sinks/wasm_filter/source/enriched_metric.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +// Per-metric name override: (type, index) -> new name. +// type: 1=counter, 2=gauge, 3=histogram. +struct NameOverride { + uint32_t type; + uint32_t index; + std::string new_name; +}; + +// Synthetic metric definition received from the WASM plugin. +struct SyntheticMetricDef { + std::string name; + uint64_t value; + Stats::TagVector tags; +}; + +// Thread-local context for the WASM filter, holding filter decisions, name +// overrides, synthetic metrics, and snapshot access for foreign functions. +struct StatsFilterContext { + absl::flat_hash_set kept_counter_indices; + absl::flat_hash_set kept_gauge_indices; + absl::flat_hash_set kept_histogram_indices; + + // True when stats_filter_emit was called by the plugin during this flush. + // Distinguishes "plugin didn't call emit" (passthrough) from "plugin + // explicitly emitted empty sets" (drop all). + bool emit_called{false}; + + // True when the stats_filter_emit call included a histogram index block. + // When false, histograms pass through unfiltered even if emit_called is true. + bool histogram_block_present{false}; + + // Per-flush name overrides set by stats_filter_set_name_overrides. + std::vector name_overrides; + + // Per-flush synthetic metrics set by stats_filter_inject_metrics. + std::vector synthetic_counters; + std::vector synthetic_gauges; + + // Non-owning pointer to the snapshot currently being flushed. + Stats::MetricSnapshot* snapshot{}; + + // Maps buffer-order index → snapshot-order index. Built before calling into + // WASM because onStatsUpdate only serializes used() metrics, but + // EnrichedMetricSnapshot indexes into the full snapshot arrays. + std::vector counter_buffer_to_snapshot; + std::vector gauge_buffer_to_snapshot; + + void clear() { + kept_counter_indices.clear(); + kept_gauge_indices.clear(); + kept_histogram_indices.clear(); + emit_called = false; + histogram_block_present = false; + name_overrides.clear(); + synthetic_counters.clear(); + synthetic_gauges.clear(); + counter_buffer_to_snapshot.clear(); + gauge_buffer_to_snapshot.clear(); + snapshot = nullptr; + } +}; + +// Returns/sets the thread-local context pointer. +StatsFilterContext* getActiveContext(); +void setActiveContext(StatsFilterContext* ctx); + +// Global tags set once by the WASM plugin at startup via +// stats_filter_set_global_tags. Stored in the sink and shared by reference +// with all enriched metric wrappers. +// Thread-local accessor for the global tags (set by stats_filter_set_global_tags). +Stats::TagVector* getGlobalTags(); +void setGlobalTags(Stats::TagVector* tags); + +// Builds the buffer-order → snapshot-order index maps needed for translating +// WASM plugin indices (which skip unused metrics) to snapshot array positions. +void buildBufferToSnapshotMaps(Stats::MetricSnapshot& snapshot, StatsFilterContext& ctx); + +// Translates WASM filter decisions from buffer-order indices to snapshot-order +// indices, applies enrichment, and flushes to the inner sink. +void processFilterDecisionsAndFlush(Stats::MetricSnapshot& snapshot, StatsFilterContext& context, + Stats::TagVector& global_tags, Stats::SymbolTable& symbol_table, + Stats::Sink& inner_sink); + +// Wraps an existing MetricSnapshot, applying: +// - Filtering by kept indices +// - Global tag injection on all metrics +// - Per-metric name overrides +// - Synthetic counter/gauge injection +class EnrichedMetricSnapshot : public Stats::MetricSnapshot { +public: + EnrichedMetricSnapshot(Stats::MetricSnapshot& original, const StatsFilterContext& ctx, + const Stats::TagVector& global_tags, Stats::SymbolTable& symbol_table); + + const std::vector& counters() override { return enriched_counters_; } + const std::vector>& gauges() override { + return enriched_gauges_; + } + const std::vector>& histograms() override { + return enriched_histograms_; + } + const std::vector>& textReadouts() override { + return original_.textReadouts(); + } + const std::vector& hostCounters() override { + return original_.hostCounters(); + } + const std::vector& hostGauges() override { + return original_.hostGauges(); + } + SystemTime snapshotTime() const override { return original_.snapshotTime(); } + +private: + Stats::MetricSnapshot& original_; + Stats::SymbolTable& symbol_table_; + + // Wrapper objects must outlive the snapshot. Stored here. + std::vector counter_wrappers_; + std::vector gauge_wrappers_; + std::vector histogram_wrappers_; + std::vector synthetic_counter_objs_; + std::vector synthetic_gauge_objs_; + + // Per-metric name overrides indexed by (type, original_index). + std::vector counter_name_overrides_; + std::vector gauge_name_overrides_; + std::vector histogram_name_overrides_; + + // Output vectors returned by accessors. + std::vector enriched_counters_; + std::vector> enriched_gauges_; + std::vector> enriched_histograms_; +}; + +// A stats sink that runs a WASM plugin as a filter/transformer/enricher before +// delegating to an inner sink. +class WasmFilterStatsSink : public Stats::Sink { +public: + WasmFilterStatsSink(Common::Wasm::PluginConfigPtr plugin_config, Stats::SinkPtr inner_sink, + Stats::SymbolTable& symbol_table, Stats::TagVector initial_global_tags = {}); + + void flush(Stats::MetricSnapshot& snapshot) override; + + void onHistogramComplete(const Stats::Histogram& histogram, uint64_t value) override { + inner_sink_->onHistogramComplete(histogram, value); + } + + Stats::TagVector& globalTags() { return global_tags_; } + +private: + Common::Wasm::PluginConfigPtr plugin_config_; + Stats::SinkPtr inner_sink_; + Stats::SymbolTable& symbol_table_; + StatsFilterContext context_; + Stats::TagVector global_tags_; +}; + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/test/BUILD b/contrib/stat_sinks/wasm_filter/test/BUILD new file mode 100644 index 0000000000000..d5464c685893c --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/BUILD @@ -0,0 +1,51 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_contrib_package", +) +load( + "//bazel:envoy_select.bzl", + "envoy_select_wasm_cpp_tests", +) + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test( + name = "wasm_filter_stat_sink_test", + srcs = ["wasm_filter_stat_sink_test.cc"], + deps = [ + "//contrib/stat_sinks/wasm_filter/source:wasm_filter_stat_sink_lib", + "//test/mocks/stats:stats_mocks", + ], +) + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//contrib/stat_sinks/wasm_filter/source:config_lib", + "//test/test_common:utility_lib", + "@envoy_api//contrib/envoy/extensions/stat_sinks/wasm_filter/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "wasm_filter_stat_sink_integration_test", + srcs = ["wasm_filter_stat_sink_integration_test.cc"], + data = envoy_select_wasm_cpp_tests([ + "//contrib/stat_sinks/wasm_filter/test/test_data:stats_filter_plugin.wasm", + ]), + rbe_pool = "6gig", + tags = ["skip_on_windows"], + deps = [ + "//contrib/stat_sinks/wasm_filter/source:wasm_filter_stat_sink_lib", + "//contrib/stat_sinks/wasm_filter/test/test_data:stats_filter_test_plugin", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/wasm_runtime/null:config", + "//test/extensions/common/wasm:wasm_runtime", + "//test/mocks/stats:stats_mocks", + "//test/test_common:wasm_lib", + ], +) diff --git a/contrib/stat_sinks/wasm_filter/test/config_test.cc b/contrib/stat_sinks/wasm_filter/test/config_test.cc new file mode 100644 index 0000000000000..fb0b81405f759 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/config_test.cc @@ -0,0 +1,44 @@ +#include "envoy/registry/registry.h" + +#include "source/common/protobuf/protobuf.h" + +#include "test/test_common/utility.h" + +#include "contrib/envoy/extensions/stat_sinks/wasm_filter/v3/wasm_filter.pb.validate.h" +#include "contrib/stat_sinks/wasm_filter/source/config.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { +namespace { + +TEST(WasmFilterSinkFactoryTest, FactoryRegistered) { + auto* factory = Registry::FactoryRegistry::getFactory( + WasmFilterName); + ASSERT_NE(factory, nullptr); + EXPECT_EQ(factory->name(), "envoy.stat_sinks.wasm_filter"); +} + +TEST(WasmFilterSinkFactoryTest, CreateEmptyConfigProto) { + WasmFilterSinkFactory factory; + auto proto = factory.createEmptyConfigProto(); + ASSERT_NE(proto, nullptr); + EXPECT_NE( + dynamic_cast( + proto.get()), + nullptr); +} + +TEST(WasmFilterSinkFactoryTest, Name) { + WasmFilterSinkFactory factory; + EXPECT_EQ(factory.name(), "envoy.stat_sinks.wasm_filter"); +} + +} // namespace +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/test/test_data/BUILD b/contrib/stat_sinks/wasm_filter/test/test_data/BUILD new file mode 100644 index 0000000000000..68417326db9b4 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/test_data/BUILD @@ -0,0 +1,32 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test_library", + "envoy_contrib_package", +) +load("//bazel/wasm:wasm.bzl", "envoy_wasm_cc_binary") + +licenses(["notice"]) # Apache 2 + +envoy_contrib_package() + +envoy_cc_test_library( + name = "stats_filter_test_plugin", + srcs = [ + "stats_filter_plugin.cc", + "stats_filter_plugin_null_plugin.cc", + ], + copts = ["-DNULL_PLUGIN=1"], + deps = [ + "//source/extensions/common/wasm:wasm_hdr", + "//source/extensions/common/wasm:wasm_lib", + "//source/extensions/common/wasm/ext:envoy_null_plugin", + ], +) + +envoy_wasm_cc_binary( + name = "stats_filter_plugin.wasm", + srcs = ["stats_filter_plugin.cc"], + deps = [ + "//source/extensions/common/wasm/ext:envoy_proxy_wasm_api_lib", + ], +) diff --git a/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin.cc b/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin.cc new file mode 100644 index 0000000000000..cce1233275029 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin.cc @@ -0,0 +1,206 @@ +// NOLINT(namespace-envoy) +// +// Test WASM stats filter plugin for envoy.stat_sinks.wasm_filter. +// Exercises all foreign functions: filtering, global tags, name overrides, +// histogram filtering, and synthetic metric injection. + +#include +#include +#include +#include + +#ifndef NULL_PLUGIN +#include "proxy_wasm_intrinsics.h" + +#include "source/extensions/common/wasm/ext/envoy_proxy_wasm_api.h" +#else +#include "source/extensions/common/wasm/ext/envoy_null_plugin.h" +#endif + +START_WASM_PLUGIN(StatsFilterTestPlugin) + +namespace { +WasmResult callForeignFunction(const std::string& name, const char* data, size_t data_size, + char** result, size_t* result_size) { + return proxy_call_foreign_function(name.data(), name.size(), data, data_size, result, + result_size); +} +} // namespace + +struct HistogramInfo { + std::string name; +}; + +class StatsFilterTestRootContext : public EnvoyRootContext { +public: + explicit StatsFilterTestRootContext(uint32_t id, std::string_view root_id) + : EnvoyRootContext(id, root_id) {} + + bool onConfigure(size_t config_size) override; + void onStatsUpdate(uint32_t result_size) override; + +private: + std::vector fetchHistograms() const; +}; + +class StatsFilterTestContext : public EnvoyContext { +public: + explicit StatsFilterTestContext(uint32_t id, RootContext* root) : EnvoyContext(id, root) {} +}; + +static RegisterContextFactory + register_StatsFilterTestContext(CONTEXT_FACTORY(StatsFilterTestContext), + ROOT_FACTORY(StatsFilterTestRootContext)); + +bool StatsFilterTestRootContext::onConfigure(size_t) { + // Set global tags via stats_filter_set_global_tags. + std::string wire; + uint32_t count = 1; + wire.append(reinterpret_cast(&count), sizeof(uint32_t)); + auto appendStr = [&](const std::string& s) { + uint32_t len = s.size(); + wire.append(reinterpret_cast(&len), sizeof(uint32_t)); + wire.append(s); + }; + appendStr("test_tag"); + appendStr("test_value"); + + char* result = nullptr; + size_t result_size = 0; + callForeignFunction("stats_filter_set_global_tags", wire.data(), wire.size(), &result, + &result_size); + logInfo("StatsFilterTestPlugin: global tags set"); + return true; +} + +std::vector StatsFilterTestRootContext::fetchHistograms() const { + char* result = nullptr; + size_t result_size = 0; + auto status = + callForeignFunction("stats_filter_get_histograms", nullptr, 0, &result, &result_size); + if (status != WasmResult::Ok || result == nullptr || result_size < sizeof(uint32_t)) { + return {}; + } + + std::vector histograms; + size_t offset = 0; + uint32_t hist_count = 0; + memcpy(&hist_count, result + offset, sizeof(uint32_t)); // NOLINT(safe-memcpy) + offset += sizeof(uint32_t); + + for (uint32_t i = 0; i < hist_count && offset < result_size; ++i) { + uint32_t name_len = 0; + memcpy(&name_len, result + offset, sizeof(uint32_t)); // NOLINT(safe-memcpy) + offset += sizeof(uint32_t); + histograms.push_back({std::string(result + offset, name_len)}); + offset += name_len; + } + + free(result); // NOLINT(cppcoreguidelines-no-malloc) + return histograms; +} + +void StatsFilterTestRootContext::onStatsUpdate(uint32_t result_size) { + auto stats_buffer = getBufferBytes(WasmBufferType::CallData, 0, result_size); + auto stats = parseStatResults(stats_buffer->view()); + + logInfo("StatsFilterTestPlugin: onStatsUpdate counters=" + std::to_string(stats.counters.size()) + + " gauges=" + std::to_string(stats.gauges.size())); + + // Keep all counters except those starting with "excluded_". + std::vector kept_counter_indices; + for (uint32_t i = 0; i < stats.counters.size(); ++i) { + std::string name(stats.counters[i].name); + if (name.substr(0, 9) != "excluded_") { + kept_counter_indices.push_back(i); + } + } + + // Keep all gauges. + std::vector kept_gauge_indices; + for (uint32_t i = 0; i < stats.gauges.size(); ++i) { + kept_gauge_indices.push_back(i); + } + + // Fetch and keep all histograms. + auto histograms = fetchHistograms(); + std::vector kept_histogram_indices; + for (uint32_t i = 0; i < histograms.size(); ++i) { + kept_histogram_indices.push_back(i); + } + + logInfo("StatsFilterTestPlugin: kept counters=" + std::to_string(kept_counter_indices.size()) + + " histograms=" + std::to_string(kept_histogram_indices.size())); + + // Set name overrides: rename first kept counter with "envoy." prefix. + if (!kept_counter_indices.empty()) { + std::string ovr_wire; + uint32_t ovr_count = 1; + ovr_wire.append(reinterpret_cast(&ovr_count), sizeof(uint32_t)); + uint32_t type = 1; // counter + ovr_wire.append(reinterpret_cast(&type), sizeof(uint32_t)); + uint32_t idx = kept_counter_indices[0]; + ovr_wire.append(reinterpret_cast(&idx), sizeof(uint32_t)); + std::string new_name = "envoy." + std::string(stats.counters[idx].name); + uint32_t name_len = new_name.size(); + ovr_wire.append(reinterpret_cast(&name_len), sizeof(uint32_t)); + ovr_wire.append(new_name); + + char* nr = nullptr; + size_t nrs = 0; + callForeignFunction("stats_filter_set_name_overrides", ovr_wire.data(), ovr_wire.size(), &nr, + &nrs); + } + + // Inject one synthetic counter. + { + std::string inj_wire; + auto appendU32 = [&](uint32_t v) { + inj_wire.append(reinterpret_cast(&v), sizeof(uint32_t)); + }; + auto appendU64 = [&](uint64_t v) { + inj_wire.append(reinterpret_cast(&v), sizeof(uint64_t)); + }; + auto appendString = [&](const std::string& s) { + appendU32(s.size()); + inj_wire.append(s); + }; + + appendU32(1); // 1 synthetic counter + appendString("wasm_filter.metrics_kept"); + appendU64(static_cast(kept_counter_indices.size())); + appendU32(0); // no tags + + appendU32(0); // 0 synthetic gauges + + char* ir = nullptr; + size_t irs = 0; + callForeignFunction("stats_filter_inject_metrics", inj_wire.data(), inj_wire.size(), &ir, &irs); + } + + // Emit kept indices. + { + std::vector wire; + wire.push_back(static_cast(kept_counter_indices.size())); + for (auto idx : kept_counter_indices) { + wire.push_back(idx); + } + wire.push_back(static_cast(kept_gauge_indices.size())); + for (auto idx : kept_gauge_indices) { + wire.push_back(idx); + } + wire.push_back(static_cast(kept_histogram_indices.size())); + for (auto idx : kept_histogram_indices) { + wire.push_back(idx); + } + + char* emit_result = nullptr; + size_t emit_result_size = 0; + callForeignFunction("stats_filter_emit", reinterpret_cast(wire.data()), + wire.size() * sizeof(uint32_t), &emit_result, &emit_result_size); + } + + logInfo("StatsFilterTestPlugin: emit complete"); +} + +END_WASM_PLUGIN diff --git a/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin_null_plugin.cc b/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin_null_plugin.cc new file mode 100644 index 0000000000000..a9510d90fc70d --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin_null_plugin.cc @@ -0,0 +1,15 @@ +// NOLINT(namespace-envoy) +#include "include/proxy-wasm/null_plugin.h" + +namespace proxy_wasm { +namespace null_plugin { +namespace StatsFilterTestPlugin { +NullPluginRegistry* context_registry_; +} // namespace StatsFilterTestPlugin + +RegisterNullVmPluginFactory register_stats_filter_test_plugin("StatsFilterTestPlugin", []() { + return std::make_unique(StatsFilterTestPlugin::context_registry_); +}); + +} // namespace null_plugin +} // namespace proxy_wasm diff --git a/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_integration_test.cc b/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_integration_test.cc new file mode 100644 index 0000000000000..2c92e369fa4e8 --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_integration_test.cc @@ -0,0 +1,108 @@ +#include "source/extensions/common/wasm/wasm.h" + +#include "test/extensions/common/wasm/wasm_runtime.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/wasm_base.h" + +#include "contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Eq; +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { + +class TestContext : public ::Envoy::Extensions::Common::Wasm::Context { +public: + using ::Envoy::Extensions::Common::Wasm::Context::Context; + ~TestContext() override = default; + using ::Envoy::Extensions::Common::Wasm::Context::log; + proxy_wasm::WasmResult log(uint32_t level, std::string_view message) override { + std::cerr << message << "\n"; + log_(static_cast(level), toAbslStringView(message)); + Extensions::Common::Wasm::Context::log(static_cast(level), message); + return proxy_wasm::WasmResult::Ok; + } + MOCK_METHOD(void, log_, (spdlog::level::level_enum level, absl::string_view message)); +}; + +class WasmFilterStatSinkIntegrationTest + : public Common::Wasm::WasmTestBase< + testing::TestWithParam>> { +public: + WasmFilterStatSinkIntegrationTest() = default; + + void setup(const std::string& code, std::string root_id = "") { + setRootId(root_id); + setupBase(std::get<0>(GetParam()), code, + [](Common::Wasm::Wasm* wasm, const std::shared_ptr& plugin) + -> proxy_wasm::ContextBase* { return new NiceMock(wasm, plugin); }); + } + + TestContext& rootContext() { return *static_cast(root_context_); } + + void setupSnapshot() { + kept_counter_.name_ = "upstream_rq_2xx"; + kept_counter_.latch_ = 10; + kept_counter_.used_ = true; + + excluded_counter_.name_ = "excluded_debug_stat"; + excluded_counter_.latch_ = 5; + excluded_counter_.used_ = true; + + gauge_.name_ = "membership_total"; + gauge_.value_ = 42; + gauge_.used_ = true; + + snapshot_.counters_ = {{10, kept_counter_}, {5, excluded_counter_}}; + snapshot_.gauges_ = {gauge_}; + + ON_CALL(snapshot_, counters()).WillByDefault(testing::ReturnRef(snapshot_.counters_)); + ON_CALL(snapshot_, gauges()).WillByDefault(testing::ReturnRef(snapshot_.gauges_)); + ON_CALL(snapshot_, histograms()).WillByDefault(testing::ReturnRef(snapshot_.histograms_)); + ON_CALL(snapshot_, textReadouts()).WillByDefault(testing::ReturnRef(snapshot_.text_readouts_)); + ON_CALL(snapshot_, hostCounters()).WillByDefault(testing::ReturnRef(snapshot_.host_counters_)); + ON_CALL(snapshot_, hostGauges()).WillByDefault(testing::ReturnRef(snapshot_.host_gauges_)); + } + + NiceMock kept_counter_; + NiceMock excluded_counter_; + NiceMock gauge_; + NiceMock snapshot_; +}; + +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmFilterStatSinkIntegrationTest, + Envoy::Extensions::Common::Wasm::runtime_and_cpp_values, + Envoy::Extensions::Common::Wasm::wasmTestParamsToString); + +TEST_P(WasmFilterStatSinkIntegrationTest, PluginSetsGlobalTagsAndFiltersMetrics) { + std::string code; + if (std::get<0>(GetParam()) != "null") { + code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + absl::StrCat("{{ test_rundir " + "}}/contrib/stat_sinks/wasm_filter/test/test_data/stats_filter_plugin.wasm"))); + } else { + code = "StatsFilterTestPlugin"; + } + EXPECT_FALSE(code.empty()); + setup(code); + setupSnapshot(); + + EXPECT_CALL(rootContext(), log_(spdlog::level::info, + Eq("StatsFilterTestPlugin: onStatsUpdate counters=2 gauges=1"))); + EXPECT_CALL(rootContext(), + log_(spdlog::level::info, Eq("StatsFilterTestPlugin: kept counters=1 histograms=0"))); + EXPECT_CALL(rootContext(), log_(spdlog::level::info, Eq("StatsFilterTestPlugin: emit complete"))); + + rootContext().onStatsUpdate(snapshot_); +} + +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_test.cc b/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_test.cc new file mode 100644 index 0000000000000..2bf040a7fbefe --- /dev/null +++ b/contrib/stat_sinks/wasm_filter/test/wasm_filter_stat_sink_test.cc @@ -0,0 +1,1490 @@ +#include "test/mocks/stats/mocks.h" + +#include "contrib/stat_sinks/wasm_filter/source/wasm_filter_stat_sink_impl.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace StatSinks { +namespace WasmFilter { +namespace { + +// ==================================================================== +// Helpers +// ==================================================================== + +void appendU32(std::string& buf, uint32_t v) { + buf.append(reinterpret_cast(&v), sizeof(uint32_t)); +} + +void appendU64(std::string& buf, uint64_t v) { + buf.append(reinterpret_cast(&v), sizeof(uint64_t)); +} + +void appendStr(std::string& buf, const std::string& s) { + appendU32(buf, s.size()); + buf.append(s); +} + +proxy_wasm::WasmForeignFunction getFF(const std::string& name) { + return proxy_wasm::getForeignFunction(name); +} + +proxy_wasm::WasmResult callFF(const std::string& name, const std::string& args, + const std::function& alloc = nullptr) { + auto ff = getFF(name); + if (ff == nullptr) { + return proxy_wasm::WasmResult::NotFound; + } + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto real_alloc = alloc ? alloc : [](size_t) -> void* { return nullptr; }; + return ff(wasm_base, args, real_alloc); +} + +// ==================================================================== +// Enriched Metric Snapshot fixture +// ==================================================================== + +class EnrichedMetricSnapshotTest : public testing::Test { +protected: + void SetUp() override { + counter_a_.name_ = "upstream_rq_2xx"; + counter_a_.latch_ = 10; + counter_a_.used_ = true; + counter_a_.setTags({{"envoy.cluster_name", "local_service"}}); + + counter_b_.name_ = "upstream_rq_5xx"; + counter_b_.latch_ = 5; + counter_b_.used_ = true; + + counter_c_.name_ = "upstream_rq_total"; + counter_c_.latch_ = 15; + counter_c_.used_ = true; + + gauge_a_.name_ = "membership_total"; + gauge_a_.value_ = 100; + gauge_a_.used_ = true; + gauge_a_.setTags({{"envoy.cluster_name", "local_service"}}); + + gauge_b_.name_ = "connections_active"; + gauge_b_.value_ = 42; + gauge_b_.used_ = true; + + histogram_a_.name_ = "upstream_rq_time"; + histogram_a_.used_ = true; + histogram_a_.setTags({{"envoy.cluster_name", "local_service"}}); + + histogram_b_.name_ = "downstream_rq_time"; + histogram_b_.used_ = true; + + snapshot_.counters_ = {{10, counter_a_}, {5, counter_b_}, {15, counter_c_}}; + snapshot_.gauges_ = {gauge_a_, gauge_b_}; + snapshot_.histograms_ = {histogram_a_, histogram_b_}; + + ON_CALL(snapshot_, counters()).WillByDefault(testing::ReturnRef(snapshot_.counters_)); + ON_CALL(snapshot_, gauges()).WillByDefault(testing::ReturnRef(snapshot_.gauges_)); + ON_CALL(snapshot_, histograms()).WillByDefault(testing::ReturnRef(snapshot_.histograms_)); + ON_CALL(snapshot_, textReadouts()).WillByDefault(testing::ReturnRef(snapshot_.text_readouts_)); + ON_CALL(snapshot_, hostCounters()).WillByDefault(testing::ReturnRef(snapshot_.host_counters_)); + ON_CALL(snapshot_, hostGauges()).WillByDefault(testing::ReturnRef(snapshot_.host_gauges_)); + } + + StatsFilterContext makeCtxKeepAll() { + StatsFilterContext ctx; + ctx.kept_counter_indices = {0, 1, 2}; + ctx.kept_gauge_indices = {0, 1}; + ctx.kept_histogram_indices = {0, 1}; + ctx.emit_called = true; + ctx.histogram_block_present = true; + ctx.snapshot = &snapshot_; + return ctx; + } + + NiceMock counter_a_; + NiceMock counter_b_; + NiceMock counter_c_; + NiceMock gauge_a_; + NiceMock gauge_b_; + NiceMock histogram_a_; + NiceMock histogram_b_; + NiceMock snapshot_; + Stats::TagVector empty_tags_; + Stats::SymbolTable& symbol_table_{counter_a_.symbolTable()}; +}; + +// ==================================================================== +// Filtering tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, KeepAll) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.counters().size(), 3); + EXPECT_EQ(enriched.gauges().size(), 2); + EXPECT_EQ(enriched.histograms().size(), 2); +} + +TEST_F(EnrichedMetricSnapshotTest, DropAll) { + StatsFilterContext ctx; + ctx.kept_counter_indices = {}; + ctx.kept_gauge_indices = {}; + ctx.kept_histogram_indices = {999}; + ctx.emit_called = true; + ctx.histogram_block_present = true; + ctx.snapshot = &snapshot_; + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.counters().size(), 0); + EXPECT_EQ(enriched.gauges().size(), 0); + EXPECT_EQ(enriched.histograms().size(), 0); +} + +TEST_F(EnrichedMetricSnapshotTest, KeepSubsetOfCounters) { + StatsFilterContext ctx; + ctx.kept_counter_indices = {0, 2}; + ctx.kept_gauge_indices = {0, 1}; + ctx.snapshot = &snapshot_; + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + ASSERT_EQ(enriched.counters().size(), 2); + EXPECT_EQ(enriched.counters()[0].counter_.get().name(), "upstream_rq_2xx"); + EXPECT_EQ(enriched.counters()[0].delta_, 10); + EXPECT_EQ(enriched.counters()[1].counter_.get().name(), "upstream_rq_total"); +} + +TEST_F(EnrichedMetricSnapshotTest, EmptyHistogramIndicesPassAllThrough) { + StatsFilterContext ctx; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {0}; + ctx.snapshot = &snapshot_; + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.histograms().size(), 2); +} + +// ==================================================================== +// Global tag injection tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, GlobalTagsAppliedToAllMetrics) { + Stats::TagVector global_tags = {{"datacenter", "us-east-1"}, {"pod", "pod42"}}; + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, global_tags, symbol_table_); + + auto counter_tags = enriched.counters()[0].counter_.get().tags(); + EXPECT_GE(counter_tags.size(), 2); + EXPECT_EQ(counter_tags[counter_tags.size() - 2].name_, "datacenter"); + EXPECT_EQ(counter_tags[counter_tags.size() - 2].value_, "us-east-1"); + EXPECT_EQ(counter_tags[counter_tags.size() - 1].name_, "pod"); + EXPECT_EQ(counter_tags[counter_tags.size() - 1].value_, "pod42"); + + auto gauge_tags = enriched.gauges()[0].get().tags(); + EXPECT_GE(gauge_tags.size(), 2); + EXPECT_EQ(gauge_tags[gauge_tags.size() - 1].name_, "pod"); + + auto hist_tags = enriched.histograms()[0].get().tags(); + EXPECT_GE(hist_tags.size(), 2); + EXPECT_EQ(hist_tags[hist_tags.size() - 1].name_, "pod"); +} + +// ==================================================================== +// Name override tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, NameOverrideOnCounter) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({1, 0, "envoy.upstream_rq_2xx"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.counters()[0].counter_.get().name(), "envoy.upstream_rq_2xx"); + EXPECT_EQ(enriched.counters()[1].counter_.get().name(), "upstream_rq_5xx"); +} + +TEST_F(EnrichedMetricSnapshotTest, NameOverrideOnGauge) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({2, 1, "envoy.connections_active"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.gauges()[1].get().name(), "envoy.connections_active"); +} + +TEST_F(EnrichedMetricSnapshotTest, NameOverrideOnHistogram) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({3, 0, "envoy.upstream_rq_time"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.histograms()[0].get().name(), "envoy.upstream_rq_time"); +} + +TEST_F(EnrichedMetricSnapshotTest, NameOverrideOutOfRange) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({1, 999, "out_of_range"}); + ctx.name_overrides.push_back({2, 999, "out_of_range"}); + ctx.name_overrides.push_back({3, 999, "out_of_range"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(enriched.counters()[0].counter_.get().name(), "upstream_rq_2xx"); + EXPECT_EQ(enriched.gauges()[0].get().name(), "membership_total"); + EXPECT_EQ(enriched.histograms()[0].get().name(), "upstream_rq_time"); +} + +// ==================================================================== +// Synthetic metric injection tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, SyntheticCounterInjected) { + auto ctx = makeCtxKeepAll(); + ctx.synthetic_counters.push_back({"wasm_filter.metrics_emitted", 42, {}}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + ASSERT_EQ(enriched.counters().size(), 4); + EXPECT_EQ(enriched.counters()[3].counter_.get().name(), "wasm_filter.metrics_emitted"); + EXPECT_EQ(enriched.counters()[3].counter_.get().value(), 42); + EXPECT_EQ(enriched.counters()[3].delta_, 42); +} + +TEST_F(EnrichedMetricSnapshotTest, SyntheticGaugeInjected) { + auto ctx = makeCtxKeepAll(); + ctx.synthetic_gauges.push_back({"app.version", 1, {{"version", "v1.2.3"}}}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + ASSERT_EQ(enriched.gauges().size(), 3); + EXPECT_EQ(enriched.gauges()[2].get().name(), "app.version"); + EXPECT_EQ(enriched.gauges()[2].get().value(), 1); + auto tags = enriched.gauges()[2].get().tags(); + ASSERT_EQ(tags.size(), 1); + EXPECT_EQ(tags[0].name_, "version"); + EXPECT_EQ(tags[0].value_, "v1.2.3"); +} + +TEST_F(EnrichedMetricSnapshotTest, SyntheticMetricsGetGlobalTags) { + Stats::TagVector global_tags = {{"dc", "us-east-1"}}; + auto ctx = makeCtxKeepAll(); + ctx.synthetic_counters.push_back({"custom.count", 10, {}}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, global_tags, symbol_table_); + + auto last = enriched.counters().back(); + auto tags = last.counter_.get().tags(); + ASSERT_GE(tags.size(), 1); + EXPECT_EQ(tags.back().name_, "dc"); +} + +// ==================================================================== +// Snapshot forwarding tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, ForwardsTextReadoutsHostCountersHostGauges) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + EXPECT_EQ(&enriched.textReadouts(), &snapshot_.text_readouts_); + EXPECT_EQ(&enriched.hostCounters(), &snapshot_.host_counters_); + EXPECT_EQ(&enriched.hostGauges(), &snapshot_.host_gauges_); +} + +TEST_F(EnrichedMetricSnapshotTest, ForwardsSnapshotTime) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + enriched.snapshotTime(); +} + +// ==================================================================== +// Combined test +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, FilterRenameTagInjectCombined) { + Stats::TagVector global_tags = {{"env", "prod"}}; + StatsFilterContext ctx; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {1}; + ctx.snapshot = &snapshot_; + ctx.name_overrides.push_back({1, 0, "envoy.upstream_rq_2xx"}); + ctx.synthetic_gauges.push_back({"wasm_filter.queue_depth", 7, {}}); + + EnrichedMetricSnapshot enriched(snapshot_, ctx, global_tags, symbol_table_); + + ASSERT_EQ(enriched.counters().size(), 1); + EXPECT_EQ(enriched.counters()[0].counter_.get().name(), "envoy.upstream_rq_2xx"); + auto ctags = enriched.counters()[0].counter_.get().tags(); + EXPECT_EQ(ctags.back().name_, "env"); + + ASSERT_EQ(enriched.gauges().size(), 2); + EXPECT_EQ(enriched.gauges()[0].get().name(), "connections_active"); + EXPECT_EQ(enriched.gauges()[1].get().name(), "wasm_filter.queue_depth"); + + EXPECT_EQ(enriched.histograms().size(), 2); +} + +// ==================================================================== +// Enriched wrapper method tests +// ==================================================================== + +TEST_F(EnrichedMetricSnapshotTest, EnrichedCounterMethods) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({1, 0, "envoy.upstream_rq_2xx"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& cref = enriched.counters()[0].counter_.get(); + EXPECT_EQ(cref.name(), "envoy.upstream_rq_2xx"); + EXPECT_EQ(cref.tagExtractedName(), "envoy.upstream_rq_2xx"); + EXPECT_TRUE(cref.used()); + EXPECT_FALSE(cref.hidden()); + EXPECT_EQ(cref.use_count(), 1); + EXPECT_FALSE(cref.tags().empty()); + + auto& mut_cref = const_cast(cref); + mut_cref.add(0); + mut_cref.inc(); + EXPECT_EQ(mut_cref.latch(), 0); + mut_cref.reset(); + mut_cref.incRefCount(); + EXPECT_FALSE(mut_cref.decRefCount()); + mut_cref.markUnused(); +} + +TEST_F(EnrichedMetricSnapshotTest, EnrichedCounterOriginalName) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& cref = enriched.counters()[0].counter_.get(); + EXPECT_EQ(cref.name(), "upstream_rq_2xx"); + EXPECT_EQ(cref.tagExtractedName(), "upstream_rq_2xx"); +} + +TEST_F(EnrichedMetricSnapshotTest, EnrichedGaugeMethods) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({2, 0, "envoy.membership"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& gref = enriched.gauges()[0].get(); + EXPECT_EQ(gref.name(), "envoy.membership"); + EXPECT_EQ(gref.tagExtractedName(), "envoy.membership"); + EXPECT_EQ(gref.value(), 100); + EXPECT_TRUE(gref.used()); + EXPECT_FALSE(gref.hidden()); + EXPECT_EQ(gref.use_count(), 1); + EXPECT_FALSE(gref.tags().empty()); + + auto& mut_gref = const_cast(gref); + mut_gref.add(0); + mut_gref.dec(); + mut_gref.inc(); + mut_gref.set(0); + mut_gref.sub(0); + mut_gref.setParentValue(0); + mut_gref.importMode(); + mut_gref.mergeImportMode(Stats::Gauge::ImportMode::NeverImport); + mut_gref.incRefCount(); + EXPECT_FALSE(mut_gref.decRefCount()); + mut_gref.markUnused(); +} + +TEST_F(EnrichedMetricSnapshotTest, EnrichedGaugeOriginalName) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& gref = enriched.gauges()[0].get(); + EXPECT_EQ(gref.name(), "membership_total"); + EXPECT_EQ(gref.tagExtractedName(), "membership_total"); +} + +TEST_F(EnrichedMetricSnapshotTest, EnrichedHistogramMethods) { + auto ctx = makeCtxKeepAll(); + ctx.name_overrides.push_back({3, 0, "envoy.upstream_rq_time"}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& href = enriched.histograms()[0].get(); + EXPECT_EQ(href.name(), "envoy.upstream_rq_time"); + EXPECT_EQ(href.tagExtractedName(), "envoy.upstream_rq_time"); + EXPECT_TRUE(href.used()); + EXPECT_FALSE(href.hidden()); + EXPECT_EQ(href.use_count(), 1); + EXPECT_FALSE(href.tags().empty()); + href.unit(); + href.quantileSummary(); + href.bucketSummary(); + href.detailedTotalBuckets(); + href.detailedIntervalBuckets(); + href.cumulativeCountLessThanOrEqualToValue(1.0); + href.intervalStatistics(); + href.cumulativeStatistics(); + + auto& mut_href = const_cast(href); + mut_href.recordValue(0); + mut_href.merge(); + mut_href.incRefCount(); + EXPECT_FALSE(mut_href.decRefCount()); + mut_href.markUnused(); +} + +TEST_F(EnrichedMetricSnapshotTest, EnrichedHistogramOriginalName) { + auto ctx = makeCtxKeepAll(); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& href = enriched.histograms()[0].get(); + EXPECT_EQ(href.name(), "upstream_rq_time"); + EXPECT_EQ(href.tagExtractedName(), "upstream_rq_time"); +} + +TEST_F(EnrichedMetricSnapshotTest, SyntheticCounterMethods) { + auto ctx = makeCtxKeepAll(); + ctx.synthetic_counters.push_back({"syn.count", 77, {{"k", "v"}}}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& sc = enriched.counters().back().counter_.get(); + EXPECT_EQ(sc.name(), "syn.count"); + EXPECT_EQ(sc.tagExtractedName(), "syn.count"); + EXPECT_EQ(sc.value(), 77); + EXPECT_TRUE(sc.used()); + EXPECT_FALSE(sc.hidden()); + EXPECT_EQ(sc.use_count(), 1); + EXPECT_EQ(sc.tags().size(), 1); + sc.constSymbolTable(); + + auto& mut_sc = const_cast(sc); + mut_sc.add(0); + mut_sc.inc(); + EXPECT_EQ(mut_sc.latch(), 0); + mut_sc.reset(); + mut_sc.symbolTable(); + mut_sc.incRefCount(); + EXPECT_FALSE(mut_sc.decRefCount()); + mut_sc.markUnused(); + mut_sc.iterateTagStatNames([](Stats::StatName, Stats::StatName) { return true; }); +} + +TEST_F(EnrichedMetricSnapshotTest, SyntheticGaugeMethods) { + auto ctx = makeCtxKeepAll(); + ctx.synthetic_gauges.push_back({"syn.gauge", 33, {{"x", "y"}}}); + EnrichedMetricSnapshot enriched(snapshot_, ctx, empty_tags_, symbol_table_); + + const auto& sg = enriched.gauges().back().get(); + EXPECT_EQ(sg.name(), "syn.gauge"); + EXPECT_EQ(sg.tagExtractedName(), "syn.gauge"); + EXPECT_EQ(sg.value(), 33); + EXPECT_TRUE(sg.used()); + EXPECT_FALSE(sg.hidden()); + EXPECT_EQ(sg.use_count(), 1); + EXPECT_EQ(sg.tags().size(), 1); + sg.constSymbolTable(); + EXPECT_EQ(sg.importMode(), Stats::Gauge::ImportMode::NeverImport); + + auto& mut_sg = const_cast(sg); + mut_sg.add(0); + mut_sg.dec(); + mut_sg.inc(); + mut_sg.set(0); + mut_sg.sub(0); + mut_sg.setParentValue(0); + mut_sg.mergeImportMode(Stats::Gauge::ImportMode::NeverImport); + mut_sg.symbolTable(); + mut_sg.incRefCount(); + EXPECT_FALSE(mut_sg.decRefCount()); + mut_sg.markUnused(); + mut_sg.iterateTagStatNames([](Stats::StatName, Stats::StatName) { return true; }); +} + +// ==================================================================== +// MergeTags utility +// ==================================================================== + +TEST(MergeTagsTest, EmptyExtraReturnsOriginal) { + Stats::TagVector original = {{"a", "1"}}; + Stats::TagVector extra = {}; + auto merged = mergeTags(original, extra); + ASSERT_EQ(merged.size(), 1); + EXPECT_EQ(merged[0].name_, "a"); +} + +TEST(MergeTagsTest, MergesBothVectors) { + Stats::TagVector original = {{"a", "1"}}; + Stats::TagVector extra = {{"b", "2"}, {"c", "3"}}; + auto merged = mergeTags(original, extra); + ASSERT_EQ(merged.size(), 3); + EXPECT_EQ(merged[0].name_, "a"); + EXPECT_EQ(merged[1].name_, "b"); + EXPECT_EQ(merged[2].name_, "c"); +} + +// ==================================================================== +// StatsFilterContext tests +// ==================================================================== + +TEST(StatsFilterContextAccessorTest, ThreadLocalAccessors) { + EXPECT_EQ(getActiveContext(), nullptr); + + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(getActiveContext(), &ctx); + + setActiveContext(nullptr); + EXPECT_EQ(getActiveContext(), nullptr); +} + +TEST(StatsFilterContextAccessorTest, GlobalTagsAccessors) { + EXPECT_EQ(getGlobalTags(), nullptr); + + Stats::TagVector tags = {{"k", "v"}}; + setGlobalTags(&tags); + EXPECT_EQ(getGlobalTags(), &tags); + + setGlobalTags(nullptr); + EXPECT_EQ(getGlobalTags(), nullptr); +} + +TEST(StatsFilterContextAccessorTest, ClearResetsEverything) { + StatsFilterContext ctx; + ctx.kept_counter_indices.insert(1); + ctx.kept_gauge_indices.insert(2); + ctx.kept_histogram_indices.insert(3); + ctx.emit_called = true; + ctx.histogram_block_present = true; + ctx.name_overrides.push_back({1, 0, "foo"}); + ctx.synthetic_counters.push_back({"bar", 1, {}}); + ctx.synthetic_gauges.push_back({"baz", 2, {}}); + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + NiceMock snap; + ctx.snapshot = &snap; + + ctx.clear(); + + EXPECT_TRUE(ctx.kept_counter_indices.empty()); + EXPECT_TRUE(ctx.kept_gauge_indices.empty()); + EXPECT_TRUE(ctx.kept_histogram_indices.empty()); + EXPECT_FALSE(ctx.emit_called); + EXPECT_FALSE(ctx.histogram_block_present); + EXPECT_TRUE(ctx.name_overrides.empty()); + EXPECT_TRUE(ctx.synthetic_counters.empty()); + EXPECT_TRUE(ctx.synthetic_gauges.empty()); + EXPECT_TRUE(ctx.counter_buffer_to_snapshot.empty()); + EXPECT_TRUE(ctx.gauge_buffer_to_snapshot.empty()); + EXPECT_EQ(ctx.snapshot, nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_emit +// ==================================================================== + +TEST(StatsFilterEmitTest, ParseCountersGaugesAndHistograms) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::vector wire = {2, 0, 2, 1, 1, 1, 0}; + std::string arguments(reinterpret_cast(wire.data()), wire.size() * sizeof(uint32_t)); + + EXPECT_EQ(callFF("stats_filter_emit", arguments), proxy_wasm::WasmResult::Ok); + + EXPECT_EQ(ctx.kept_counter_indices.size(), 2); + EXPECT_TRUE(ctx.kept_counter_indices.contains(0)); + EXPECT_TRUE(ctx.kept_counter_indices.contains(2)); + EXPECT_EQ(ctx.kept_gauge_indices.size(), 1); + EXPECT_TRUE(ctx.kept_gauge_indices.contains(1)); + EXPECT_EQ(ctx.kept_histogram_indices.size(), 1); + EXPECT_TRUE(ctx.kept_histogram_indices.contains(0)); + EXPECT_TRUE(ctx.emit_called); + EXPECT_TRUE(ctx.histogram_block_present); + + setActiveContext(nullptr); +} + +TEST(StatsFilterEmitTest, HistogramBlockIsOptional) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::vector wire = {1, 0, 0}; + std::string arguments(reinterpret_cast(wire.data()), wire.size() * sizeof(uint32_t)); + + EXPECT_EQ(callFF("stats_filter_emit", arguments), proxy_wasm::WasmResult::Ok); + EXPECT_EQ(ctx.kept_counter_indices.size(), 1); + EXPECT_TRUE(ctx.kept_histogram_indices.empty()); + EXPECT_TRUE(ctx.emit_called); + EXPECT_FALSE(ctx.histogram_block_present); + + setActiveContext(nullptr); +} + +TEST(StatsFilterEmitTest, NoActiveContext) { + setActiveContext(nullptr); + + std::vector wire = {0, 0}; + std::string arguments(reinterpret_cast(wire.data()), wire.size() * sizeof(uint32_t)); + + EXPECT_EQ(callFF("stats_filter_emit", arguments), proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterEmitTest, EmptyArguments) { + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(callFF("stats_filter_emit", ""), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterEmitTest, TruncatedCounterBlock) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 5); + appendU32(wire, 0); + + EXPECT_EQ(callFF("stats_filter_emit", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterEmitTest, TruncatedGaugeBlock) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 0); + appendU32(wire, 10); + appendU32(wire, 0); + + EXPECT_EQ(callFF("stats_filter_emit", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_set_global_tags +// ==================================================================== + +TEST(StatsFilterGlobalTagsTest, SetGlobalTagsWithActivePointer) { + Stats::TagVector tags; + setGlobalTags(&tags); + + std::string wire; + appendU32(wire, 2); + appendStr(wire, "dc"); + appendStr(wire, "us"); + appendStr(wire, "pod"); + appendStr(wire, "p42"); + + EXPECT_EQ(callFF("stats_filter_set_global_tags", wire), proxy_wasm::WasmResult::Ok); + + ASSERT_EQ(tags.size(), 2); + EXPECT_EQ(tags[0].name_, "dc"); + EXPECT_EQ(tags[0].value_, "us"); + EXPECT_EQ(tags[1].name_, "pod"); + EXPECT_EQ(tags[1].value_, "p42"); + + setGlobalTags(nullptr); +} + +TEST(StatsFilterGlobalTagsTest, SetGlobalTagsNullPointerReturnsFailure) { + setGlobalTags(nullptr); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "env"); + appendStr(wire, "staging"); + + EXPECT_EQ(callFF("stats_filter_set_global_tags", wire), proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterGlobalTagsTest, SetGlobalTagsBadArgument) { + Stats::TagVector tags; + setGlobalTags(&tags); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "key"); + + EXPECT_EQ(callFF("stats_filter_set_global_tags", wire), proxy_wasm::WasmResult::BadArgument); + setGlobalTags(nullptr); +} + +TEST(StatsFilterGlobalTagsTest, ActivePointerClearsBeforeReuse) { + Stats::TagVector tags = {{"old", "val"}}; + setGlobalTags(&tags); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "new"); + appendStr(wire, "val"); + + EXPECT_EQ(callFF("stats_filter_set_global_tags", wire), proxy_wasm::WasmResult::Ok); + ASSERT_EQ(tags.size(), 1); + EXPECT_EQ(tags[0].name_, "new"); + + setGlobalTags(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_set_name_overrides +// ==================================================================== + +TEST(StatsFilterNameOverridesTest, SetNameOverrides) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 1); + appendU32(wire, 1); + appendU32(wire, 0); + appendStr(wire, "envoy.rq"); + + EXPECT_EQ(callFF("stats_filter_set_name_overrides", wire), proxy_wasm::WasmResult::Ok); + + ASSERT_EQ(ctx.name_overrides.size(), 1); + EXPECT_EQ(ctx.name_overrides[0].type, 1); + EXPECT_EQ(ctx.name_overrides[0].index, 0); + EXPECT_EQ(ctx.name_overrides[0].new_name, "envoy.rq"); + + setActiveContext(nullptr); +} + +TEST(StatsFilterNameOverridesTest, NoActiveContext) { + setActiveContext(nullptr); + std::string wire; + appendU32(wire, 0); + EXPECT_EQ(callFF("stats_filter_set_name_overrides", wire), + proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterNameOverridesTest, BadArguments) { + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(callFF("stats_filter_set_name_overrides", ""), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterNameOverridesTest, TruncatedOverride) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 1); + appendU32(wire, 1); + + EXPECT_EQ(callFF("stats_filter_set_name_overrides", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterNameOverridesTest, TruncatedName) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 1); + appendU32(wire, 1); + appendU32(wire, 0); + appendU32(wire, 100); + + EXPECT_EQ(callFF("stats_filter_set_name_overrides", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_inject_metrics +// ==================================================================== + +TEST(StatsFilterInjectMetricsTest, InjectCounterAndGauge) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "custom.count"); + appendU64(wire, 99); + appendU32(wire, 0); + appendU32(wire, 1); + appendStr(wire, "custom.gauge"); + appendU64(wire, 7); + appendU32(wire, 1); + appendStr(wire, "k"); + appendStr(wire, "v"); + + EXPECT_EQ(callFF("stats_filter_inject_metrics", wire), proxy_wasm::WasmResult::Ok); + + ASSERT_EQ(ctx.synthetic_counters.size(), 1); + EXPECT_EQ(ctx.synthetic_counters[0].name, "custom.count"); + EXPECT_EQ(ctx.synthetic_counters[0].value, 99); + + ASSERT_EQ(ctx.synthetic_gauges.size(), 1); + EXPECT_EQ(ctx.synthetic_gauges[0].name, "custom.gauge"); + EXPECT_EQ(ctx.synthetic_gauges[0].value, 7); + ASSERT_EQ(ctx.synthetic_gauges[0].tags.size(), 1); + EXPECT_EQ(ctx.synthetic_gauges[0].tags[0].name_, "k"); + EXPECT_EQ(ctx.synthetic_gauges[0].tags[0].value_, "v"); + + setActiveContext(nullptr); +} + +TEST(StatsFilterInjectMetricsTest, NoActiveContext) { + setActiveContext(nullptr); + std::string wire; + appendU32(wire, 0); + appendU32(wire, 0); + EXPECT_EQ(callFF("stats_filter_inject_metrics", wire), proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterInjectMetricsTest, TruncatedCounterBlock) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "name"); + + EXPECT_EQ(callFF("stats_filter_inject_metrics", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterInjectMetricsTest, TruncatedGaugeBlock) { + StatsFilterContext ctx; + setActiveContext(&ctx); + + std::string wire; + appendU32(wire, 0); + appendU32(wire, 1); + appendStr(wire, "name"); + + EXPECT_EQ(callFF("stats_filter_inject_metrics", wire), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +TEST(StatsFilterInjectMetricsTest, EmptyInput) { + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(callFF("stats_filter_inject_metrics", ""), proxy_wasm::WasmResult::BadArgument); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_get_metric_tags +// ==================================================================== + +class GetMetricTagsTest : public EnrichedMetricSnapshotTest { +protected: + void SetUp() override { + EnrichedMetricSnapshotTest::SetUp(); + ctx_.snapshot = &snapshot_; + ctx_.counter_buffer_to_snapshot = {0, 1, 2}; + ctx_.gauge_buffer_to_snapshot = {0, 1}; + setActiveContext(&ctx_); + } + void TearDown() override { setActiveContext(nullptr); } + + StatsFilterContext ctx_; +}; + +TEST_F(GetMetricTagsTest, GetCounterTags) { + std::string wire; + appendU32(wire, 1); + appendU32(wire, 0); + + std::string result_buf; + auto alloc = [&result_buf](size_t size) -> void* { + result_buf.resize(size); + return result_buf.data(); + }; + + auto ff = getFF("stats_filter_get_metric_tags"); + ASSERT_TRUE(ff != nullptr); + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto r = ff(wasm_base, wire, alloc); + EXPECT_EQ(r, proxy_wasm::WasmResult::Ok); + EXPECT_GT(result_buf.size(), sizeof(uint32_t)); + + uint32_t tag_count = 0; + memcpy(&tag_count, result_buf.data(), sizeof(uint32_t)); + EXPECT_EQ(tag_count, 1); +} + +TEST_F(GetMetricTagsTest, GetGaugeTags) { + std::string wire; + appendU32(wire, 2); + appendU32(wire, 0); + + std::string result_buf; + auto alloc = [&result_buf](size_t size) -> void* { + result_buf.resize(size); + return result_buf.data(); + }; + + auto ff = getFF("stats_filter_get_metric_tags"); + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto r = ff(wasm_base, wire, alloc); + EXPECT_EQ(r, proxy_wasm::WasmResult::Ok); + + uint32_t tag_count = 0; + memcpy(&tag_count, result_buf.data(), sizeof(uint32_t)); + EXPECT_EQ(tag_count, 1); +} + +TEST_F(GetMetricTagsTest, GetHistogramTags) { + std::string wire; + appendU32(wire, 3); + appendU32(wire, 0); + + std::string result_buf; + auto alloc = [&result_buf](size_t size) -> void* { + result_buf.resize(size); + return result_buf.data(); + }; + + auto ff = getFF("stats_filter_get_metric_tags"); + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto r = ff(wasm_base, wire, alloc); + EXPECT_EQ(r, proxy_wasm::WasmResult::Ok); + + uint32_t tag_count = 0; + memcpy(&tag_count, result_buf.data(), sizeof(uint32_t)); + EXPECT_EQ(tag_count, 1); +} + +TEST_F(GetMetricTagsTest, InvalidType) { + std::string wire; + appendU32(wire, 99); + appendU32(wire, 0); + + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::BadArgument); +} + +TEST_F(GetMetricTagsTest, CounterIndexOutOfRange) { + std::string wire; + appendU32(wire, 1); + appendU32(wire, 999); + + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::BadArgument); +} + +TEST_F(GetMetricTagsTest, GaugeIndexOutOfRange) { + std::string wire; + appendU32(wire, 2); + appendU32(wire, 999); + + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::BadArgument); +} + +TEST_F(GetMetricTagsTest, HistogramIndexOutOfRange) { + std::string wire; + appendU32(wire, 3); + appendU32(wire, 999); + + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::BadArgument); +} + +TEST_F(GetMetricTagsTest, TooShortArguments) { + std::string wire; + appendU32(wire, 1); + + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::BadArgument); +} + +TEST(StatsFilterGetMetricTagsTest, NoActiveContext) { + setActiveContext(nullptr); + std::string wire; + appendU32(wire, 1); + appendU32(wire, 0); + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterGetMetricTagsTest, NullSnapshot) { + StatsFilterContext ctx; + setActiveContext(&ctx); + std::string wire; + appendU32(wire, 1); + appendU32(wire, 0); + EXPECT_EQ(callFF("stats_filter_get_metric_tags", wire), proxy_wasm::WasmResult::InternalFailure); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_get_all_metric_tags +// ==================================================================== + +class GetAllMetricTagsTest : public EnrichedMetricSnapshotTest { +protected: + void SetUp() override { + EnrichedMetricSnapshotTest::SetUp(); + ctx_.snapshot = &snapshot_; + buildBufferToSnapshotMaps(snapshot_, ctx_); + setActiveContext(&ctx_); + } + void TearDown() override { setActiveContext(nullptr); } + + StatsFilterContext ctx_; +}; + +TEST_F(GetAllMetricTagsTest, ReturnsAllTags) { + std::string result_buf; + auto alloc = [&result_buf](size_t size) -> void* { + result_buf.resize(size); + return result_buf.data(); + }; + + auto ff = getFF("stats_filter_get_all_metric_tags"); + ASSERT_TRUE(ff != nullptr); + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto r = ff(wasm_base, "", alloc); + EXPECT_EQ(r, proxy_wasm::WasmResult::Ok); + EXPECT_GT(result_buf.size(), 3 * sizeof(uint32_t)); + + size_t offset = 0; + auto readU32 = [&](uint32_t& out) { + memcpy(&out, result_buf.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + }; + + uint32_t counter_count = 0; + readU32(counter_count); + EXPECT_EQ(counter_count, 3); + + for (uint32_t i = 0; i < counter_count; ++i) { + uint32_t tc = 0; + readU32(tc); + for (uint32_t t = 0; t < tc; ++t) { + uint32_t nlen = 0; + readU32(nlen); + offset += nlen; + uint32_t vlen = 0; + readU32(vlen); + offset += vlen; + } + } + + uint32_t gauge_count = 0; + readU32(gauge_count); + EXPECT_EQ(gauge_count, 2); +} + +TEST(StatsFilterGetAllMetricTagsTest, NoActiveContext) { + setActiveContext(nullptr); + EXPECT_EQ(callFF("stats_filter_get_all_metric_tags", ""), + proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterGetAllMetricTagsTest, NullSnapshot) { + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(callFF("stats_filter_get_all_metric_tags", ""), + proxy_wasm::WasmResult::InternalFailure); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function: stats_filter_get_histograms +// ==================================================================== + +class GetHistogramsTest : public EnrichedMetricSnapshotTest { +protected: + void SetUp() override { + EnrichedMetricSnapshotTest::SetUp(); + ctx_.snapshot = &snapshot_; + setActiveContext(&ctx_); + } + void TearDown() override { setActiveContext(nullptr); } + + StatsFilterContext ctx_; +}; + +TEST_F(GetHistogramsTest, ReturnsHistogramNames) { + std::string result_buf; + auto alloc = [&result_buf](size_t size) -> void* { + result_buf.resize(size); + return result_buf.data(); + }; + + auto ff = getFF("stats_filter_get_histograms"); + ASSERT_TRUE(ff != nullptr); + proxy_wasm::WasmBase wasm_base(nullptr, "", "", "", {}, {}); + auto r = ff(wasm_base, "", alloc); + EXPECT_EQ(r, proxy_wasm::WasmResult::Ok); + + size_t offset = 0; + uint32_t count = 0; + memcpy(&count, result_buf.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + EXPECT_EQ(count, 2); + + uint32_t name_len = 0; + memcpy(&name_len, result_buf.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::string name1(result_buf.data() + offset, name_len); + offset += name_len; + EXPECT_EQ(name1, "upstream_rq_time"); + + memcpy(&name_len, result_buf.data() + offset, sizeof(uint32_t)); + offset += sizeof(uint32_t); + std::string name2(result_buf.data() + offset, name_len); + EXPECT_EQ(name2, "downstream_rq_time"); +} + +TEST(StatsFilterGetHistogramsTest, NoActiveContext) { + setActiveContext(nullptr); + EXPECT_EQ(callFF("stats_filter_get_histograms", ""), proxy_wasm::WasmResult::InternalFailure); +} + +TEST(StatsFilterGetHistogramsTest, NullSnapshot) { + StatsFilterContext ctx; + setActiveContext(&ctx); + EXPECT_EQ(callFF("stats_filter_get_histograms", ""), proxy_wasm::WasmResult::InternalFailure); + setActiveContext(nullptr); +} + +// ==================================================================== +// Foreign function registration existence +// ==================================================================== + +TEST(StatsFilterForeignFunctionRegistration, AllFunctionsRegistered) { + EXPECT_NE(getFF("stats_filter_emit"), nullptr); + EXPECT_NE(getFF("stats_filter_set_global_tags"), nullptr); + EXPECT_NE(getFF("stats_filter_set_name_overrides"), nullptr); + EXPECT_NE(getFF("stats_filter_inject_metrics"), nullptr); + EXPECT_NE(getFF("stats_filter_get_metric_tags"), nullptr); + EXPECT_NE(getFF("stats_filter_get_all_metric_tags"), nullptr); + EXPECT_NE(getFF("stats_filter_get_histograms"), nullptr); +} + +// ==================================================================== +// buildBufferToSnapshotMaps +// ==================================================================== + +class BuildBufferToSnapshotMapsTest : public EnrichedMetricSnapshotTest {}; + +TEST_F(BuildBufferToSnapshotMapsTest, AllUsedMetrics) { + StatsFilterContext ctx; + buildBufferToSnapshotMaps(snapshot_, ctx); + + ASSERT_EQ(ctx.counter_buffer_to_snapshot.size(), 3); + EXPECT_EQ(ctx.counter_buffer_to_snapshot[0], 0); + EXPECT_EQ(ctx.counter_buffer_to_snapshot[1], 1); + EXPECT_EQ(ctx.counter_buffer_to_snapshot[2], 2); + + ASSERT_EQ(ctx.gauge_buffer_to_snapshot.size(), 2); + EXPECT_EQ(ctx.gauge_buffer_to_snapshot[0], 0); + EXPECT_EQ(ctx.gauge_buffer_to_snapshot[1], 1); +} + +TEST_F(BuildBufferToSnapshotMapsTest, MixedUsedAndUnused) { + counter_b_.used_ = false; + gauge_b_.used_ = false; + + StatsFilterContext ctx; + buildBufferToSnapshotMaps(snapshot_, ctx); + + ASSERT_EQ(ctx.counter_buffer_to_snapshot.size(), 2); + EXPECT_EQ(ctx.counter_buffer_to_snapshot[0], 0); + EXPECT_EQ(ctx.counter_buffer_to_snapshot[1], 2); + + ASSERT_EQ(ctx.gauge_buffer_to_snapshot.size(), 1); + EXPECT_EQ(ctx.gauge_buffer_to_snapshot[0], 0); +} + +TEST_F(BuildBufferToSnapshotMapsTest, NoneUsed) { + counter_a_.used_ = false; + counter_b_.used_ = false; + counter_c_.used_ = false; + gauge_a_.used_ = false; + gauge_b_.used_ = false; + + StatsFilterContext ctx; + buildBufferToSnapshotMaps(snapshot_, ctx); + + EXPECT_TRUE(ctx.counter_buffer_to_snapshot.empty()); + EXPECT_TRUE(ctx.gauge_buffer_to_snapshot.empty()); +} + +// ==================================================================== +// processFilterDecisionsAndFlush +// ==================================================================== + +class ProcessFilterDecisionsTest : public EnrichedMetricSnapshotTest { +protected: + NiceMock inner_sink_; +}; + +TEST_F(ProcessFilterDecisionsTest, NoDecisionsNoEnrichments_Passthrough) { + StatsFilterContext ctx; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::Ref(snapshot_))); + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, GlobalTagsOnly_KeepAllAndEnrich) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + Stats::TagVector global_tags = {{"dc", "us-east-1"}}; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 3); + EXPECT_EQ(s.gauges().size(), 2); + auto tags = s.counters()[0].counter_.get().tags(); + EXPECT_EQ(tags.back().name_, "dc"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, FilterDecisions_KeepSubset) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {1}; + ctx.emit_called = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 1); + EXPECT_EQ(s.counters()[0].counter_.get().name(), "upstream_rq_2xx"); + EXPECT_EQ(s.gauges().size(), 1); + EXPECT_EQ(s.gauges()[0].get().name(), "connections_active"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, FilterDecisions_UnusedMetricsPassThrough) { + counter_c_.used_ = false; + gauge_b_.used_ = false; + + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1}; + ctx.gauge_buffer_to_snapshot = {0}; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {0}; + ctx.emit_called = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 2); + EXPECT_EQ(s.gauges().size(), 2); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, IndexTranslation) { + counter_b_.used_ = false; + + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {1}; + ctx.kept_gauge_indices = {0, 1}; + ctx.emit_called = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + bool found_total = false; + bool found_5xx = false; + for (const auto& c : s.counters()) { + if (c.counter_.get().name() == "upstream_rq_total") { + found_total = true; + } + if (c.counter_.get().name() == "upstream_rq_5xx") { + found_5xx = true; + } + } + EXPECT_TRUE(found_total); + EXPECT_TRUE(found_5xx); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, NameOverrideTranslation) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {0, 1, 2}; + ctx.kept_gauge_indices = {0, 1}; + ctx.emit_called = true; + ctx.name_overrides.push_back({1, 0, "envoy.rq_2xx"}); + ctx.name_overrides.push_back({2, 0, "envoy.membership"}); + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters()[0].counter_.get().name(), "envoy.rq_2xx"); + EXPECT_EQ(s.gauges()[0].get().name(), "envoy.membership"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, SyntheticMetrics_NoFilterDecisions) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.synthetic_counters.push_back({"custom.metric", 100, {}}); + ctx.synthetic_gauges.push_back({"custom.gauge", 50, {}}); + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 4); + EXPECT_EQ(s.counters().back().counter_.get().name(), "custom.metric"); + EXPECT_EQ(s.gauges().size(), 3); + EXPECT_EQ(s.gauges().back().get().name(), "custom.gauge"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, HistogramFilterDecisions) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {0, 1, 2}; + ctx.kept_gauge_indices = {0, 1}; + ctx.kept_histogram_indices = {0}; + ctx.emit_called = true; + ctx.histogram_block_present = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.histograms().size(), 1); + EXPECT_EQ(s.histograms()[0].get().name(), "upstream_rq_time"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, OutOfRangeBufferIndicesIgnored) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0}; + ctx.gauge_buffer_to_snapshot = {0}; + ctx.kept_counter_indices = {0, 99}; + ctx.kept_gauge_indices = {0, 99}; + ctx.emit_called = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)); + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, NameOverrideOutOfRangeMapping) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0}; + ctx.gauge_buffer_to_snapshot = {0}; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {0}; + ctx.emit_called = true; + ctx.name_overrides.push_back({1, 99, "wont_apply"}); + ctx.name_overrides.push_back({2, 99, "wont_apply"}); + ctx.name_overrides.push_back({3, 0, "histogram_rename"}); + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)); + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, CombinedFilterEnrichSyntheticOverride) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {0, 2}; + ctx.kept_gauge_indices = {0}; + ctx.emit_called = true; + ctx.name_overrides.push_back({1, 0, "envoy.rq_2xx"}); + ctx.synthetic_counters.push_back({"wasm.count", 5, {{"source", "plugin"}}}); + Stats::TagVector global_tags = {{"env", "prod"}}; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 3); + EXPECT_EQ(s.counters()[0].counter_.get().name(), "envoy.rq_2xx"); + auto tags = s.counters()[0].counter_.get().tags(); + EXPECT_EQ(tags.back().name_, "env"); + + EXPECT_EQ(s.counters().back().counter_.get().name(), "wasm.count"); + auto syn_tags = s.counters().back().counter_.get().tags(); + EXPECT_GE(syn_tags.size(), 2); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, EmitCalledWithEmptySets_DropsAllUsedMetrics) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.emit_called = true; + ctx.histogram_block_present = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 0); + EXPECT_EQ(s.gauges().size(), 0); + EXPECT_EQ(s.histograms().size(), 0); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, EmitCalledWithEmptySets_UnusedMetricsStillPassThrough) { + counter_c_.used_ = false; + gauge_b_.used_ = false; + + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1}; + ctx.gauge_buffer_to_snapshot = {0}; + ctx.emit_called = true; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 1); + EXPECT_EQ(s.counters()[0].counter_.get().name(), "upstream_rq_total"); + EXPECT_EQ(s.gauges().size(), 1); + EXPECT_EQ(s.gauges()[0].get().name(), "connections_active"); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +TEST_F(ProcessFilterDecisionsTest, EmitCalledNoHistogramBlock_HistogramsPassThrough) { + StatsFilterContext ctx; + ctx.counter_buffer_to_snapshot = {0, 1, 2}; + ctx.gauge_buffer_to_snapshot = {0, 1}; + ctx.kept_counter_indices = {0}; + ctx.kept_gauge_indices = {0}; + ctx.emit_called = true; + ctx.histogram_block_present = false; + Stats::TagVector global_tags; + + EXPECT_CALL(inner_sink_, flush(testing::_)) + .WillOnce(testing::Invoke([](Stats::MetricSnapshot& s) { + EXPECT_EQ(s.counters().size(), 1); + EXPECT_EQ(s.gauges().size(), 1); + EXPECT_EQ(s.histograms().size(), 2); + })); + + processFilterDecisionsAndFlush(snapshot_, ctx, global_tags, symbol_table_, inner_sink_); +} + +// ==================================================================== +// Foreign function: stats_filter_set_global_tags (null pointer) +// ==================================================================== + +TEST(StatsFilterGlobalTagsTest, NullPointerBadWire) { + setGlobalTags(nullptr); + + std::string wire; + appendU32(wire, 1); + appendStr(wire, "key"); + + EXPECT_EQ(callFF("stats_filter_set_global_tags", wire), proxy_wasm::WasmResult::InternalFailure); +} + +} // namespace +} // namespace WasmFilter +} // namespace StatSinks +} // namespace Extensions +} // namespace Envoy diff --git a/contrib/sxg/filters/http/source/filter_config.cc b/contrib/sxg/filters/http/source/filter_config.cc index 8d0fb33600175..6456c15d4f1df 100644 --- a/contrib/sxg/filters/http/source/filter_config.cc +++ b/contrib/sxg/filters/http/source/filter_config.cc @@ -34,10 +34,10 @@ FilterConfig::FilterConfig(const envoy::extensions::filters::http::sxg::v3alpha: duration_(proto_config.has_duration() ? proto_config.duration().seconds() : 604800UL), cbor_url_(proto_config.cbor_url()), validity_url_(proto_config.validity_url()), mi_record_size_(proto_config.mi_record_size() ? proto_config.mi_record_size() : 4096L), - client_can_accept_sxg_header_(proto_config.client_can_accept_sxg_header().length() > 0 + client_can_accept_sxg_header_(!proto_config.client_can_accept_sxg_header().empty() ? proto_config.client_can_accept_sxg_header() : "x-client-can-accept-sxg"), - should_encode_sxg_header_(proto_config.should_encode_sxg_header().length() > 0 + should_encode_sxg_header_(!proto_config.should_encode_sxg_header().empty() ? proto_config.should_encode_sxg_header() : "x-should-encode-sxg"), header_prefix_filters_(initializeHeaderPrefixFilters(proto_config.header_prefix_filters())), diff --git a/contrib/sxg/filters/http/test/BUILD b/contrib/sxg/filters/http/test/BUILD index 7180c2b6cf112..0f94de970f848 100644 --- a/contrib/sxg/filters/http/test/BUILD +++ b/contrib/sxg/filters/http/test/BUILD @@ -36,6 +36,7 @@ envoy_cc_test( "//source/common/secret:secret_manager_impl_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", "//test/integration:http_integration_lib", + "//test/mocks/secret:secret_mocks", "//test/mocks/server:server_mocks", "//test/mocks/upstream:upstream_mocks", "@envoy_api//contrib/envoy/extensions/filters/http/sxg/v3alpha:pkg_cc_proto", diff --git a/contrib/sxg/filters/http/test/config_test.cc b/contrib/sxg/filters/http/test/config_test.cc index c54517630bc93..25c45ece3946b 100644 --- a/contrib/sxg/filters/http/test/config_test.cc +++ b/contrib/sxg/filters/http/test/config_test.cc @@ -5,6 +5,7 @@ #include "source/common/protobuf/utility.h" #include "source/common/secret/secret_provider_impl.h" +#include "test/mocks/secret/mocks.h" #include "test/mocks/server/factory_context.h" #include "contrib/envoy/extensions/filters/http/sxg/v3alpha/sxg.pb.h" diff --git a/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc b/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc index 649a8e5aae6d6..8e292a7fc048d 100644 --- a/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc +++ b/contrib/tap_sinks/udp_sink/source/udp_sink_impl.cc @@ -234,7 +234,7 @@ void UdpTapSink::UdpTapSinkHandle::handleSocketStreamedTraceForMultiEvents( src_repeated_events->DeleteSubrange(0, 1); // Check if the remaining original trace now fits within the size limit. - if (src_repeated_events->size() > 0 && + if (!src_repeated_events->empty() && static_cast(trace->ByteSizeLong()) < max_size_of_each_sub_data) { doSubmitTrace(std::move(trace), format); return; @@ -251,7 +251,7 @@ void UdpTapSink::UdpTapSinkHandle::handleSocketStreamedTraceForMultiEvents( submitTraceAndResetVariables(new_trace, format); // Send the original trace and return if its remaining size is within the allowed limit. - if (src_repeated_events->size() > 0 && + if (!src_repeated_events->empty() && static_cast(trace->ByteSizeLong()) < max_size_of_each_sub_data) { doSubmitTrace(std::move(trace), format); return; diff --git a/contrib/tap_sinks/udp_sink/test/udp_sink_config_test.cc b/contrib/tap_sinks/udp_sink/test/udp_sink_config_test.cc index 9b3f95925b020..f62fb37290ea0 100644 --- a/contrib/tap_sinks/udp_sink/test/udp_sink_config_test.cc +++ b/contrib/tap_sinks/udp_sink/test/udp_sink_config_test.cc @@ -21,9 +21,6 @@ namespace TapSinks { namespace UDP { // Test Udp sink -using ::testing::_; -using ::testing::AtLeast; -using ::testing::Return; namespace TapCommon = Extensions::Common::Tap; diff --git a/contrib/tap_sinks/udp_sink/test/udp_sink_interation_test.cc b/contrib/tap_sinks/udp_sink/test/udp_sink_interation_test.cc index 96f1fa850eb98..98efbfc63d86f 100644 --- a/contrib/tap_sinks/udp_sink/test/udp_sink_interation_test.cc +++ b/contrib/tap_sinks/udp_sink/test/udp_sink_interation_test.cc @@ -160,11 +160,11 @@ name: tap sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; - if (inet_pton(AF_INET, UDP_SERVER_IP_, &server_addr.sin_addr) <= 0) { + if (inet_pton(AF_INET, udp_server_ip_, &server_addr.sin_addr) <= 0) { close(server_socket_); return false; } - server_addr.sin_port = htons(UDP_PORT_); + server_addr.sin_port = htons(udp_port_); if (bind(server_socket_, reinterpret_cast(&server_addr), sizeof(server_addr)) < 0) { close(server_socket_); @@ -186,13 +186,13 @@ name: tap // Return false because there is no any message. // Consider other logical if EWOULDBLOCK is occurred, and // shouldn't occur in UT env - isRcvMatchedUDPMsg_ = false; + is_rcv_matched_udp_msg_ = false; } else { // Go the message. std::string rcv_msg{buffer}; - if (rcv_msg.find(MATCHED_TAP_REQ_STR_) != std::string::npos && - rcv_msg.find(MATCHED_TAP_RESP_STR_) != std::string::npos) { - isRcvMatchedUDPMsg_ = true; + if (rcv_msg.find(matched_tap_req_str_) != std::string::npos && + rcv_msg.find(matched_tap_resp_str_) != std::string::npos) { + is_rcv_matched_udp_msg_ = true; } } } @@ -202,15 +202,15 @@ name: tap server_socket_ = -1; } - bool isUDPServerRcvMatchedUDPMsg(void) { return isRcvMatchedUDPMsg_; } + bool isUDPServerRcvMatchedUDPMsg(void) { return is_rcv_matched_udp_msg_; } private: - const int UDP_PORT_ = 8089; - const char* UDP_SERVER_IP_ = "127.0.0.1"; - const char* MATCHED_TAP_REQ_STR_ = "tapudp"; - const char* MATCHED_TAP_RESP_STR_ = "200"; + const int udp_port_ = 8089; + const char* udp_server_ip_ = "127.0.0.1"; + const char* matched_tap_req_str_ = "tapudp"; + const char* matched_tap_resp_str_ = "200"; int server_socket_ = -1; - bool isRcvMatchedUDPMsg_ = false; + bool is_rcv_matched_udp_msg_ = false; }; // Start UDP server firstly. @@ -224,7 +224,7 @@ name: tap codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); makeRequest(request_headers_udp_tap_, {}, nullptr, response_headers_udp_tap_, {}, nullptr); codec_client_->close(); - test_server_->waitForCounterGe("http.config_test.downstream_cx_destroy", 1); + test_server_->waitForCounter("http.config_test.downstream_cx_destroy", testing::Ge(1)); // Verify whether get the expect message tap_server.checkRcvedUDPMsg(); diff --git a/contrib/tap_sinks/udp_sink/test/udp_sink_test.cc b/contrib/tap_sinks/udp_sink/test/udp_sink_test.cc index ab52636f04f65..6153b19f8d364 100644 --- a/contrib/tap_sinks/udp_sink/test/udp_sink_test.cc +++ b/contrib/tap_sinks/udp_sink/test/udp_sink_test.cc @@ -19,13 +19,11 @@ namespace TapSinks { namespace UDP { // Test Udp sink -using ::testing::_; -using ::testing::Return; class UdpTapSinkTest : public testing::Test { protected: - UdpTapSinkTest() {} - ~UdpTapSinkTest() {} + UdpTapSinkTest() = default; + ~UdpTapSinkTest() override = default; public: envoy::config::core::v3::SocketAddress socket_address_; @@ -103,8 +101,8 @@ TEST_F(UdpTapSinkTest, TestSubmitTraceForNotSUpportedFormat) { // re Mock class UdpPacketWriter and not use existing Network::MockUdpPacketWriter class MockUdpPacketWriterNew : public Network::UdpPacketWriter { public: - MockUdpPacketWriterNew(bool isReturnOk) : isReturnOkForwritePacket_(isReturnOk) {}; - ~MockUdpPacketWriterNew() override {}; + MockUdpPacketWriterNew(bool is_return_ok) : is_return_ok_forwrite_packet_(is_return_ok) {} + ~MockUdpPacketWriterNew() override = default; Api::IoCallUint64Result writePacket(const Buffer::Instance& buffer, const Network::Address::Ip* local_ip, @@ -112,10 +110,10 @@ class MockUdpPacketWriterNew : public Network::UdpPacketWriter { (void)buffer; (void)local_ip; (void)peer_address; - if (isReturnOkForwritePacket_) { - return Api::IoCallUint64Result(99, Api::IoError::none()); + if (is_return_ok_forwrite_packet_) { + return {99, Api::IoError::none()}; } else { - return Api::IoCallUint64Result(0, Network::IoSocketError::getIoSocketEagainError()); + return {0, Network::IoSocketError::getIoSocketEagainError()}; } } MOCK_METHOD(bool, isWriteBlocked, (), (const)); @@ -129,16 +127,16 @@ class MockUdpPacketWriterNew : public Network::UdpPacketWriter { MOCK_METHOD(Api::IoCallUint64Result, flush, ()); private: - const bool isReturnOkForwritePacket_; + const bool is_return_ok_forwrite_packet_; }; class UtSpecialUdpTapSink : public UdpTapSink { public: UtSpecialUdpTapSink(const envoy::extensions::tap_sinks::udp_sink::v3alpha::UdpSink& config) : UdpTapSink(config) {} - ~UtSpecialUdpTapSink() {} - void replaceOrigUdpPacketWriter(Network::UdpPacketWriterPtr&& utUdpPacketWriter) { - udp_packet_writer_ = std::move(utUdpPacketWriter); + ~UtSpecialUdpTapSink() override = default; + void replaceOrigUdpPacketWriter(Network::UdpPacketWriterPtr&& ut_udp_packet_writer) { + udp_packet_writer_ = std::move(ut_udp_packet_writer); } }; diff --git a/contrib/vcl/source/vcl_io_handle.cc b/contrib/vcl/source/vcl_io_handle.cc index 667d329f0dda6..a4e4e946fbc6d 100644 --- a/contrib/vcl/source/vcl_io_handle.cc +++ b/contrib/vcl/source/vcl_io_handle.cc @@ -1,5 +1,7 @@ #include "contrib/vcl/source/vcl_io_handle.h" +#include + #include "source/common/buffer/buffer_impl.h" #include "source/common/network/address_impl.h" @@ -48,13 +50,13 @@ void vclEndptCopy(sockaddr* addr, socklen_t* addrlen, const vppcom_endpt_t& ep) if (ep.is_ip4) { sockaddr_in* addr4 = reinterpret_cast(addr); addr4->sin_family = AF_INET; - *addrlen = std::min(static_cast(sizeof(struct sockaddr_in)), *addrlen); + *addrlen = std::min(static_cast(sizeof(struct in_addr)), *addrlen); memcpy(&addr4->sin_addr, ep.ip, *addrlen); // NOLINT(safe-memcpy) addr4->sin_port = ep.port; } else { sockaddr_in6* addr6 = reinterpret_cast(addr); addr6->sin6_family = AF_INET6; - *addrlen = std::min(static_cast(sizeof(struct sockaddr_in6)), *addrlen); + *addrlen = std::min(static_cast(sizeof(struct in6_addr)), *addrlen); memcpy(&addr6->sin6_addr, ep.ip, *addrlen); // NOLINT(safe-memcpy) addr6->sin6_port = ep.port; } @@ -67,13 +69,13 @@ Envoy::Network::Address::InstanceConstSharedPtr vclEndptToAddress(const vppcom_e if (ep.is_ip4) { addr.ss_family = AF_INET; - len = sizeof(struct sockaddr_in); + len = sizeof(struct in_addr); auto in4 = reinterpret_cast(&addr); memcpy(&in4->sin_addr, ep.ip, len); // NOLINT(safe-memcpy) in4->sin_port = ep.port; } else { addr.ss_family = AF_INET6; - len = sizeof(struct sockaddr_in6); + len = sizeof(struct in6_addr); auto in6 = reinterpret_cast(&addr); memcpy(&in6->sin6_addr, ep.ip, len); // NOLINT(safe-memcpy) in6->sin6_port = ep.port; @@ -197,7 +199,7 @@ Api::IoCallUint64Result VclIoHandle::readv(uint64_t max_length, Buffer::RawSlice } #if VCL_RX_ZC -Api::IoCallUint64Result VclIoHandle::read(Buffer::Instance& buffer, absl::optional) { +Api::IoCallUint64Result VclIoHandle::read(Buffer::Instance& buffer, std::optional) { vppcom_data_segment_t ds[16]; int32_t rv; @@ -227,7 +229,7 @@ Api::IoCallUint64Result VclIoHandle::read(Buffer::Instance& buffer, absl::option } #else Api::IoCallUint64Result VclIoHandle::read(Buffer::Instance& buffer, - absl::optional max_length_opt) { + std::optional max_length_opt) { uint64_t max_length = max_length_opt.value_or(UINT64_MAX); if (max_length == 0) { return Api::ioCallUint64ResultNoError(); @@ -620,7 +622,7 @@ Api::SysCallIntResult VclIoHandle::setBlocking(bool) { return {rv < 0 ? -1 : 0, -rv}; } -absl::optional VclIoHandle::domain() { +std::optional VclIoHandle::domain() { VCL_LOG("grabbing domain sh {:x}", sh_); return {AF_INET}; }; @@ -769,9 +771,9 @@ IoHandlePtr VclIoHandle::duplicate() { return io_handle; } -absl::optional VclIoHandle::lastRoundTripTime() { return {}; } +std::optional VclIoHandle::lastRoundTripTime() { return {}; } -absl::optional VclIoHandle::congestionWindowInBytes() const { return {}; } +std::optional VclIoHandle::congestionWindowInBytes() const { return {}; } } // namespace Vcl } // namespace Network diff --git a/contrib/vcl/source/vcl_io_handle.h b/contrib/vcl/source/vcl_io_handle.h index 94d502cc0fec7..ac40a418a0cee 100644 --- a/contrib/vcl/source/vcl_io_handle.h +++ b/contrib/vcl/source/vcl_io_handle.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/api/io_error.h" #include "envoy/common/exception.h" @@ -35,7 +36,7 @@ class VclIoHandle : public Envoy::Network::IoHandle, Api::IoCallUint64Result readv(uint64_t max_length, Buffer::RawSlice* slices, uint64_t num_slice) override; Api::IoCallUint64Result read(Buffer::Instance& buffer, - absl::optional max_length) override; + std::optional max_length) override; Api::IoCallUint64Result writev(const Buffer::RawSlice* slices, uint64_t num_slice) override; Api::IoCallUint64Result write(Buffer::Instance& buffer) override; Api::IoCallUint64Result recv(void* buffer, size_t length, int flags) override; @@ -48,8 +49,8 @@ class VclIoHandle : public Envoy::Network::IoHandle, Api::IoCallUint64Result recvmmsg(RawSliceArrays& slices, uint32_t self_port, const UdpSaveCmsgConfig& save_cmsg_config, RecvMsgOutput& output) override; - absl::optional lastRoundTripTime() override; - absl::optional congestionWindowInBytes() const override; + std::optional lastRoundTripTime() override; + std::optional congestionWindowInBytes() const override; bool supportsMmsg() const override; bool supportsUdpGro() const override { return false; } @@ -65,7 +66,7 @@ class VclIoHandle : public Envoy::Network::IoHandle, unsigned long in_buffer_len, void* out_buffer, unsigned long out_buffer_len, unsigned long* bytes_returned) override; Api::SysCallIntResult setBlocking(bool blocking) override; - absl::optional domain() override; + std::optional domain() override; absl::StatusOr localAddress() override; absl::StatusOr peerAddress() override; Api::SysCallIntResult shutdown(int) override { return {0, 0}; } @@ -77,7 +78,7 @@ class VclIoHandle : public Envoy::Network::IoHandle, void resetFileEvents() override; IoHandlePtr duplicate() override; - absl::optional interfaceName() override { return absl::nullopt; } + std::optional interfaceName() override { return std::nullopt; } void cb(uint32_t events) { THROW_IF_NOT_OK(cb_(events)); } void setCb(Event::FileReadyCb cb) { cb_ = cb; } diff --git a/distribution/BUILD b/distribution/BUILD index a66d8d5bbf366..4ca6e1d6c37b3 100644 --- a/distribution/BUILD +++ b/distribution/BUILD @@ -5,6 +5,12 @@ load(":packages.bzl", "envoy_pkg_distros") licenses(["notice"]) # Apache 2 +# IMPORTANT: SECRET-HANDLING TARGETS +# +# Several targets in this file process release-signing material using the GPG signing key +# +# Any target handling secrets MUST NOT be cached or executed remotely. + envoy_package() MAINTAINER = "Envoy maintainers " @@ -106,6 +112,7 @@ label_flag( build_setting_default = ":placeholder", ) +# NOTE: HANDLES SECRETS genrule( name = "multi_arch_debs", srcs = [ @@ -136,8 +143,13 @@ genrule( tar cf $$tmpdir2/debs.tar.gz -C "$${tmpdir}" . tar cf $@ -C "$${tmpdir2}" . """, + tags = [ + "no-remote", + "no-remote-cache", + ], ) +# NOTE: HANDLES SECRETS genrule( name = "signed", srcs = [ @@ -169,6 +181,9 @@ genrule( -m arm64/envoy-contrib:bin/envoy-contrib-$${VERSION}-linux-aarch_64 \ --out $@ """ % VERSION, - tags = ["no-remote"], + tags = [ + "no-remote", + "no-remote-cache", + ], tools = ["//tools/distribution:sign"], ) diff --git a/distribution/docker/Dockerfile-envoy b/distribution/docker/Dockerfile-envoy index 68600b3a7d2e9..061b1828a2d1a 100644 --- a/distribution/docker/Dockerfile-envoy +++ b/distribution/docker/Dockerfile-envoy @@ -1,6 +1,6 @@ ARG BUILD_OS=ubuntu ARG BUILD_TAG=22.04 -ARG BUILD_SHA=eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 +ARG BUILD_SHA=4f838adc7181d9039ac795a7d0aba05a9bd9ecd480d294483169c5def983b64d ARG ENVOY_VRP_BASE_IMAGE=envoy-base @@ -59,7 +59,7 @@ COPY --chown=0:0 --chmod=755 \ # STAGE: envoy-distroless -FROM gcr.io/distroless/base-nossl-debian12:nonroot@sha256:177f4df07b055157cc1114033c1e531b251c8f7ef5ef17e1248dc3a52ec4de60 AS envoy-distroless +FROM gcr.io/distroless/base-nossl-debian13:nonroot@sha256:20f009ee6f6976a13763db818c6525f6f3882b73a32b5bff3a672d9de5beb5ca AS envoy-distroless EXPOSE 10000 ENTRYPOINT ["/usr/local/bin/envoy"] CMD ["-c", "/etc/envoy/envoy.yaml"] diff --git a/distribution/docker/build.sh b/distribution/docker/build.sh index a9985bd09e3f5..a5d10d617d57d 100755 --- a/distribution/docker/build.sh +++ b/distribution/docker/build.sh @@ -48,7 +48,7 @@ fi # Setting environments for buildx tools config_env() { - BUILDKIT_VERSION=$(grep '^FROM moby/buildkit:' ci/Dockerfile-buildkit | cut -d ':' -f2) + BUILDKIT_VERSION=$(grep '^FROM moby/buildkit:' ci/Dockerfile-buildkit | cut -d ':' -f2-) echo ">> BUILDX: install ${BUILDKIT_VERSION}" if [[ "${DOCKER_PLATFORM}" == *","* ]]; then diff --git a/docs/BUILD b/docs/BUILD index 810456e91dfe9..f8961bcbe6221 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -35,6 +35,7 @@ filegroup( "root/configuration/listeners/network_filters/_include/generic_proxy_filter.yaml", "root/configuration/overview/_include/xds_api/oauth-sds-example.yaml", "root/configuration/security/_include/sds-source-example.yaml", + "root/configuration/http/http_filters/_include/ip-tagging-filter.yaml", ], ) + select({ "@envoy//bazel:windows_x86_64": [], @@ -192,7 +193,7 @@ genrule( "//tools:generate_version_histories", "@envoy//:VERSION.txt", "@envoy//changelogs", - "@envoy//changelogs:sections.yaml", + "@envoy//changelogs:changelogs.yaml", ], ) diff --git a/docs/Cargo.Bazel.lock b/docs/Cargo.Bazel.lock index 5e3db0befa05c..b94a9991e883e 100644 --- a/docs/Cargo.Bazel.lock +++ b/docs/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "5b28fd0dce18a76ab2908a4493989dbf92fe211266706e299c5a49874f95892d", + "checksum": "ab1bf27b6172ab6ff094cc63e1db922d26015dc64095d0fb29443ca48168cf20", "crates": { "aho-corasick 1.1.4": { "name": "aho-corasick", @@ -259,6 +259,45 @@ ], "license_file": "LICENSE-APACHE" }, + "autocfg 1.5.0": { + "name": "autocfg", + "version": "1.5.0", + "package_url": "https://github.com/cuviper/autocfg", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/autocfg/1.5.0/download", + "sha256": "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + } + }, + "targets": [ + { + "Library": { + "crate_name": "autocfg", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "autocfg", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "1.5.0" + }, + "license": "Apache-2.0 OR MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "bindgen 0.70.1": { "name": "bindgen", "version": "0.70.1", @@ -667,6 +706,71 @@ ], "license_file": "LICENSE-APACHE" }, + "chacha20 0.10.0": { + "name": "chacha20", + "version": "0.10.0", + "package_url": "https://github.com/RustCrypto/stream-ciphers", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/chacha20/0.10.0/download", + "sha256": "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" + } + }, + "targets": [ + { + "Library": { + "crate_name": "chacha20", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "chacha20", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "rng" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "cfg-if 1.0.4", + "target": "cfg_if" + }, + { + "id": "rand_core 0.10.1", + "target": "rand_core" + } + ], + "selects": { + "cfg(any(target_arch = \"x86_64\", target_arch = \"x86\"))": [ + { + "id": "cpufeatures 0.3.0", + "target": "cpufeatures" + } + ] + } + }, + "edition": "2024", + "version": "0.10.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "clang-sys 1.8.1": { "name": "clang-sys", "version": "1.8.1", @@ -774,20 +878,20 @@ ], "license_file": "LICENSE.txt" }, - "critical-section 1.2.0": { - "name": "critical-section", - "version": "1.2.0", - "package_url": "https://github.com/rust-embedded/critical-section", + "combine 4.6.7": { + "name": "combine", + "version": "4.6.7", + "package_url": "https://github.com/Marwes/combine", "repository": { "Http": { - "url": "https://static.crates.io/crates/critical-section/1.2.0/download", - "sha256": "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + "url": "https://static.crates.io/crates/combine/4.6.7/download", + "sha256": "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" } }, "targets": [ { "Library": { - "crate_name": "critical_section", + "crate_name": "combine", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -798,35 +902,43 @@ } } ], - "library_target_name": "critical_section", + "library_target_name": "combine", "common_attrs": { "compile_data_glob": [ "**" ], + "deps": { + "common": [ + { + "id": "memchr 2.8.0", + "target": "memchr" + } + ], + "selects": {} + }, "edition": "2018", - "version": "1.2.0" + "version": "4.6.7" }, - "license": "MIT OR Apache-2.0", + "license": "MIT", "license_ids": [ - "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": "LICENSE" }, - "crossbeam-channel 0.5.15": { - "name": "crossbeam-channel", - "version": "0.5.15", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "core-foundation 0.9.4": { + "name": "core-foundation", + "version": "0.9.4", + "package_url": "https://github.com/servo/core-foundation-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-channel/0.5.15/download", - "sha256": "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" + "url": "https://static.crates.io/crates/core-foundation/0.9.4/download", + "sha256": "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_channel", + "crate_name": "core_foundation", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -837,7 +949,7 @@ } } ], - "library_target_name": "crossbeam_channel", + "library_target_name": "core_foundation", "common_attrs": { "compile_data_glob": [ "**" @@ -845,21 +957,25 @@ "crate_features": { "common": [ "default", - "std" + "link" ], "selects": {} }, "deps": { "common": [ { - "id": "crossbeam-utils 0.8.21", - "target": "crossbeam_utils" + "id": "core-foundation-sys 0.8.7", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.183", + "target": "libc" } ], "selects": {} }, - "edition": "2021", - "version": "0.5.15" + "edition": "2018", + "version": "0.9.4" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -868,20 +984,20 @@ ], "license_file": "LICENSE-APACHE" }, - "crossbeam-epoch 0.9.18": { - "name": "crossbeam-epoch", - "version": "0.9.18", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "core-foundation-sys 0.8.7": { + "name": "core-foundation-sys", + "version": "0.8.7", + "package_url": "https://github.com/servo/core-foundation-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-epoch/0.9.18/download", - "sha256": "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" + "url": "https://static.crates.io/crates/core-foundation-sys/0.8.7/download", + "sha256": "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_epoch", + "crate_name": "core_foundation_sys", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -892,30 +1008,20 @@ } } ], - "library_target_name": "crossbeam_epoch", + "library_target_name": "core_foundation_sys", "common_attrs": { "compile_data_glob": [ "**" ], "crate_features": { "common": [ - "alloc", "default", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "crossbeam-utils 0.8.21", - "target": "crossbeam_utils" - } + "link" ], "selects": {} }, - "edition": "2021", - "version": "0.9.18" + "edition": "2018", + "version": "0.8.7" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -924,20 +1030,20 @@ ], "license_file": "LICENSE-APACHE" }, - "crossbeam-utils 0.8.21": { - "name": "crossbeam-utils", - "version": "0.8.21", - "package_url": "https://github.com/crossbeam-rs/crossbeam", + "cpufeatures 0.3.0": { + "name": "cpufeatures", + "version": "0.3.0", + "package_url": "https://github.com/RustCrypto/utils", "repository": { "Http": { - "url": "https://static.crates.io/crates/crossbeam-utils/0.8.21/download", - "sha256": "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + "url": "https://static.crates.io/crates/cpufeatures/0.3.0/download", + "sha256": "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" } }, "targets": [ { "Library": { - "crate_name": "crossbeam_utils", + "crate_name": "cpufeatures", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -946,54 +1052,44 @@ ] } } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } } ], - "library_target_name": "crossbeam_utils", + "library_target_name": "cpufeatures", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "default", - "std" - ], - "selects": {} - }, "deps": { - "common": [ - { - "id": "crossbeam-utils 0.8.21", - "target": "build_script_build" - } - ], - "selects": {} + "common": [], + "selects": { + "cfg(all(target_arch = \"aarch64\", target_os = \"android\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"aarch64\", target_vendor = \"apple\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ], + "cfg(all(target_arch = \"loongarch64\", target_os = \"linux\"))": [ + { + "id": "libc 0.2.183", + "target": "libc" + } + ] + } }, - "edition": "2021", - "version": "0.8.21" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] + "edition": "2024", + "version": "0.3.0" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -1002,20 +1098,20 @@ ], "license_file": "LICENSE-APACHE" }, - "data-encoding 2.10.0": { - "name": "data-encoding", - "version": "2.10.0", - "package_url": "https://github.com/ia0/data-encoding", + "critical-section 1.2.0": { + "name": "critical-section", + "version": "1.2.0", + "package_url": "https://github.com/rust-embedded/critical-section", "repository": { "Http": { - "url": "https://static.crates.io/crates/data-encoding/2.10.0/download", - "sha256": "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + "url": "https://static.crates.io/crates/critical-section/1.2.0/download", + "sha256": "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" } }, "targets": [ { "Library": { - "crate_name": "data_encoding", + "crate_name": "critical_section", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -1026,35 +1122,263 @@ } } ], - "library_target_name": "data_encoding", + "library_target_name": "critical_section", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "alloc", - "std" - ], - "selects": {} - }, "edition": "2018", - "version": "2.10.0" + "version": "1.2.0" }, - "license": "MIT", + "license": "MIT OR Apache-2.0", "license_ids": [ + "Apache-2.0", "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, - "deranged 0.5.8": { - "name": "deranged", - "version": "0.5.8", - "package_url": "https://github.com/jhpratt/deranged", + "crossbeam-channel 0.5.15": { + "name": "crossbeam-channel", + "version": "0.5.15", + "package_url": "https://github.com/crossbeam-rs/crossbeam", "repository": { "Http": { - "url": "https://static.crates.io/crates/deranged/0.5.8/download", - "sha256": "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + "url": "https://static.crates.io/crates/crossbeam-channel/0.5.15/download", + "sha256": "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_channel", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_channel", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "crossbeam_utils" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.5.15" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "crossbeam-epoch 0.9.18": { + "name": "crossbeam-epoch", + "version": "0.9.18", + "package_url": "https://github.com/crossbeam-rs/crossbeam", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/crossbeam-epoch/0.9.18/download", + "sha256": "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_epoch", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_epoch", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "alloc", + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "crossbeam_utils" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.9.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "crossbeam-utils 0.8.21": { + "name": "crossbeam-utils", + "version": "0.8.21", + "package_url": "https://github.com/crossbeam-rs/crossbeam", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/crossbeam-utils/0.8.21/download", + "sha256": "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + } + }, + "targets": [ + { + "Library": { + "crate_name": "crossbeam_utils", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "crossbeam_utils", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "crossbeam-utils 0.8.21", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.8.21" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ] + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "data-encoding 2.10.0": { + "name": "data-encoding", + "version": "2.10.0", + "package_url": "https://github.com/ia0/data-encoding", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/data-encoding/2.10.0/download", + "sha256": "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + } + }, + "targets": [ + { + "Library": { + "crate_name": "data_encoding", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "data_encoding", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "alloc", + "std" + ], + "selects": {} + }, + "edition": "2018", + "version": "2.10.0" + }, + "license": "MIT", + "license_ids": [ + "MIT" + ], + "license_file": "LICENSE" + }, + "deranged 0.5.8": { + "name": "deranged", + "version": "0.5.8", + "package_url": "https://github.com/jhpratt/deranged", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/deranged/0.5.8/download", + "sha256": "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" } }, "targets": [ @@ -1232,70 +1556,17 @@ "compile_data_glob": [ "**" ], - "edition": "2021", - "version": "1.15.0" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "enum-as-inner 0.6.1": { - "name": "enum-as-inner", - "version": "0.6.1", - "package_url": "https://github.com/bluejekyll/enum-as-inner", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/enum-as-inner/0.6.1/download", - "sha256": "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" - } - }, - "targets": [ - { - "ProcMacro": { - "crate_name": "enum_as_inner", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "enum_as_inner", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { + "crate_features": { "common": [ - { - "id": "heck 0.5.0", - "target": "heck" - }, - { - "id": "proc-macro2 1.0.106", - "target": "proc_macro2" - }, - { - "id": "quote 1.0.45", - "target": "quote" - }, - { - "id": "syn 2.0.117", - "target": "syn" - } + "default", + "std" ], "selects": {} }, - "edition": "2018", - "version": "0.6.1" + "edition": "2021", + "version": "1.15.0" }, - "license": "MIT/Apache-2.0", + "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" @@ -1316,7 +1587,7 @@ "deps": { "common": [ { - "id": "hickory-resolver 0.25.2", + "id": "hickory-resolver 0.26.1", "target": "hickory_resolver" }, { @@ -1831,6 +2102,62 @@ ], "license_file": "LICENSE-APACHE" }, + "futures-macro 0.3.32": { + "name": "futures-macro", + "version": "0.3.32", + "package_url": "https://github.com/rust-lang/futures-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/futures-macro/0.3.32/download", + "sha256": "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "futures_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "futures_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "proc-macro2 1.0.106", + "target": "proc_macro2" + }, + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.3.32" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "futures-sink 0.3.32": { "name": "futures-sink", "version": "0.3.32", @@ -1956,6 +2283,9 @@ "crate_features": { "common": [ "alloc", + "async-await", + "async-await-macro", + "futures-macro", "slab", "std" ], @@ -1977,74 +2307,22 @@ }, { "id": "slab 0.4.12", - "target": "slab" - } - ], - "selects": {} - }, - "edition": "2018", - "version": "0.3.32" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "getrandom 0.2.17": { - "name": "getrandom", - "version": "0.2.17", - "package_url": "https://github.com/rust-random/getrandom", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/getrandom/0.2.17/download", - "sha256": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" - } - }, - "targets": [ - { - "Library": { - "crate_name": "getrandom", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "getrandom", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { + "target": "slab" + } + ], + "selects": {} + }, + "edition": "2018", + "proc_macro_deps": { "common": [ { - "id": "cfg-if 1.0.4", - "target": "cfg_if" + "id": "futures-macro 0.3.32", + "target": "futures_macro" } ], - "selects": { - "cfg(target_os = \"wasi\")": [ - { - "id": "wasi 0.11.1+wasi-snapshot-preview1", - "target": "wasi" - } - ], - "cfg(unix)": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ] - } + "selects": {} }, - "edition": "2018", - "version": "0.2.17" + "version": "0.3.32" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2053,14 +2331,14 @@ ], "license_file": "LICENSE-APACHE" }, - "getrandom 0.3.4": { + "getrandom 0.2.17": { "name": "getrandom", - "version": "0.3.4", + "version": "0.2.17", "package_url": "https://github.com/rust-random/getrandom", "repository": { "Http": { - "url": "https://static.crates.io/crates/getrandom/0.3.4/download", - "sha256": "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" + "url": "https://static.crates.io/crates/getrandom/0.2.17/download", + "sha256": "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" } }, "targets": [ @@ -2075,18 +2353,6 @@ ] } } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } } ], "library_target_name": "getrandom", @@ -2094,79 +2360,21 @@ "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "std" - ], - "selects": {} - }, "deps": { "common": [ { "id": "cfg-if 1.0.4", "target": "cfg_if" - }, - { - "id": "getrandom 0.3.4", - "target": "build_script_build" } ], "selects": { - "cfg(all(any(target_os = \"linux\", target_os = \"android\"), not(any(all(target_os = \"linux\", target_env = \"\"), getrandom_backend = \"custom\", getrandom_backend = \"linux_raw\", getrandom_backend = \"rdrand\", getrandom_backend = \"rndr\"))))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p2\"))": [ - { - "id": "wasip2 1.0.2+wasi-0.2.9", - "target": "wasip2" - } - ], - "cfg(all(target_os = \"uefi\", getrandom_backend = \"efi_rng\"))": [ - { - "id": "r-efi 5.3.0", - "target": "r_efi" - } - ], - "cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"hurd\", target_os = \"illumos\", target_os = \"cygwin\", all(target_os = \"horizon\", target_arch = \"arm\")))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"haiku\", target_os = \"redox\", target_os = \"nto\", target_os = \"aix\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"ios\", target_os = \"visionos\", target_os = \"watchos\", target_os = \"tvos\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(any(target_os = \"macos\", target_os = \"openbsd\", target_os = \"vita\", target_os = \"emscripten\"))": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(target_os = \"netbsd\")": [ - { - "id": "libc 0.2.183", - "target": "libc" - } - ], - "cfg(target_os = \"solaris\")": [ + "cfg(target_os = \"wasi\")": [ { - "id": "libc 0.2.183", - "target": "libc" + "id": "wasi 0.11.1+wasi-snapshot-preview1", + "target": "wasi" } ], - "cfg(target_os = \"vxworks\")": [ + "cfg(unix)": [ { "id": "libc 0.2.183", "target": "libc" @@ -2174,19 +2382,8 @@ ] } }, - "edition": "2021", - "version": "0.3.4" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] + "edition": "2018", + "version": "0.2.17" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2236,6 +2433,13 @@ "compile_data_glob": [ "**" ], + "crate_features": { + "common": [ + "std", + "sys_rng" + ], + "selects": {} + }, "deps": { "common": [ { @@ -2245,6 +2449,10 @@ { "id": "getrandom 0.4.2", "target": "build_script_build" + }, + { + "id": "rand_core 0.10.1", + "target": "rand_core" } ], "selects": { @@ -2601,20 +2809,20 @@ ], "license_file": "LICENSE-APACHE" }, - "hickory-proto 0.25.2": { - "name": "hickory-proto", - "version": "0.25.2", + "hickory-net 0.26.1": { + "name": "hickory-net", + "version": "0.26.1", "package_url": "https://github.com/hickory-dns/hickory-dns", "repository": { "Http": { - "url": "https://static.crates.io/crates/hickory-proto/0.25.2/download", - "sha256": "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" + "url": "https://static.crates.io/crates/hickory-net/0.26.1/download", + "sha256": "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" } }, "targets": [ { "Library": { - "crate_name": "hickory_proto", + "crate_name": "hickory_net", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -2625,7 +2833,7 @@ } } ], - "library_target_name": "hickory_proto", + "library_target_name": "hickory_net", "common_attrs": { "compile_data_glob": [ "**" @@ -2636,9 +2844,7 @@ "__https", "__tls", "dnssec-ring", - "futures-io", "https-ring", - "std", "tls-ring", "tokio", "webpki-roots" @@ -2679,6 +2885,10 @@ "id": "h2 0.4.13", "target": "h2" }, + { + "id": "hickory-proto 0.26.1", + "target": "hickory_proto" + }, { "id": "http 1.4.0", "target": "http" @@ -2692,11 +2902,15 @@ "target": "ipnet" }, { - "id": "once_cell 1.21.4", - "target": "once_cell" + "id": "lru-cache 0.1.2", + "target": "lru_cache" + }, + { + "id": "parking_lot 0.12.5", + "target": "parking_lot" }, { - "id": "rand 0.9.2", + "id": "rand 0.10.1", "target": "rand" }, { @@ -2740,11 +2954,18 @@ "target": "url" }, { - "id": "webpki-roots 0.26.11", + "id": "webpki-roots 1.0.6", "target": "webpki_roots" } ], - "selects": {} + "selects": { + "cfg(target_os = \"android\")": [ + { + "id": "jni 0.22.4", + "target": "jni" + } + ] + } }, "edition": "2021", "proc_macro_deps": { @@ -2752,15 +2973,127 @@ { "id": "async-trait 0.1.89", "target": "async_trait" + } + ], + "selects": {} + }, + "version": "0.26.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "hickory-proto 0.26.1": { + "name": "hickory-proto", + "version": "0.26.1", + "package_url": "https://github.com/hickory-dns/hickory-dns", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/hickory-proto/0.26.1/download", + "sha256": "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" + } + }, + "targets": [ + { + "Library": { + "crate_name": "hickory_proto", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "hickory_proto", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "__dnssec", + "access-control", + "dnssec-ring", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "bitflags 2.11.0", + "target": "bitflags" + }, + { + "id": "data-encoding 2.10.0", + "target": "data_encoding" + }, + { + "id": "idna 1.1.0", + "target": "idna" + }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, + { + "id": "once_cell 1.21.4", + "target": "once_cell" + }, + { + "id": "prefix-trie 0.8.3", + "target": "prefix_trie" + }, + { + "id": "rand 0.10.1", + "target": "rand" + }, + { + "id": "ring 0.17.14", + "target": "ring" + }, + { + "id": "rustls-pki-types 1.14.0", + "target": "rustls_pki_types" + }, + { + "id": "thiserror 2.0.18", + "target": "thiserror" + }, + { + "id": "time 0.3.47", + "target": "time" + }, + { + "id": "tinyvec 1.11.0", + "target": "tinyvec" + }, + { + "id": "tracing 0.1.44", + "target": "tracing" }, { - "id": "enum-as-inner 0.6.1", - "target": "enum_as_inner" + "id": "url 2.5.8", + "target": "url" } ], - "selects": {} + "selects": { + "cfg(target_os = \"android\")": [ + { + "id": "jni 0.22.4", + "target": "jni" + } + ] + } }, - "version": "0.25.2" + "edition": "2021", + "version": "0.26.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -2769,14 +3102,14 @@ ], "license_file": "LICENSE-APACHE" }, - "hickory-resolver 0.25.2": { + "hickory-resolver 0.26.1": { "name": "hickory-resolver", - "version": "0.25.2", + "version": "0.26.1", "package_url": "https://github.com/hickory-dns/hickory-dns", "repository": { "Http": { - "url": "https://static.crates.io/crates/hickory-resolver/0.25.2/download", - "sha256": "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" + "url": "https://static.crates.io/crates/hickory-resolver/0.26.1/download", + "sha256": "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" } }, "targets": [ @@ -2824,9 +3157,17 @@ "target": "futures_util" }, { - "id": "hickory-proto 0.25.2", + "id": "hickory-net 0.26.1", + "target": "hickory_net" + }, + { + "id": "hickory-proto 0.26.1", "target": "hickory_proto" }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, { "id": "moka 0.12.15", "target": "moka" @@ -2840,7 +3181,7 @@ "target": "parking_lot" }, { - "id": "rand 0.9.2", + "id": "rand 0.10.1", "target": "rand" }, { @@ -2872,11 +3213,17 @@ "target": "tracing" }, { - "id": "webpki-roots 0.26.11", + "id": "webpki-roots 1.0.6", "target": "webpki_roots" } ], "selects": { + "aarch64-apple-darwin": [ + { + "id": "system-configuration 0.7.0", + "target": "system_configuration" + } + ], "x86_64-pc-windows-msvc": [ { "id": "ipconfig 0.3.4", @@ -2886,7 +3233,7 @@ } }, "edition": "2021", - "version": "0.25.2" + "version": "0.26.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -3859,41 +4206,355 @@ } } ], - "library_target_name": "ipnet", + "library_target_name": "ipnet", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "serde", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "serde 1.0.228", + "target": "serde" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "2.12.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "itertools 0.13.0": { + "name": "itertools", + "version": "0.13.0", + "package_url": "https://github.com/rust-itertools/itertools", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/itertools/0.13.0/download", + "sha256": "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" + } + }, + "targets": [ + { + "Library": { + "crate_name": "itertools", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "itertools", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "either 1.15.0", + "target": "either" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.13.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "itoa 1.0.18": { + "name": "itoa", + "version": "1.0.18", + "package_url": "https://github.com/dtolnay/itoa", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/itoa/1.0.18/download", + "sha256": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + } + }, + "targets": [ + { + "Library": { + "crate_name": "itoa", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "itoa", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2021", + "version": "1.0.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "jni 0.22.4": { + "name": "jni", + "version": "0.22.4", + "package_url": "https://github.com/jni-rs/jni-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/jni/0.22.4/download", + "sha256": "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" + } + }, + "targets": [ + { + "Library": { + "crate_name": "jni", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "jni", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "cfg-if 1.0.4", + "target": "cfg_if" + }, + { + "id": "combine 4.6.7", + "target": "combine" + }, + { + "id": "jni 0.22.4", + "target": "build_script_build" + }, + { + "id": "jni-sys 0.4.1", + "target": "jni_sys" + }, + { + "id": "log 0.4.29", + "target": "log" + }, + { + "id": "simd_cesu8 1.1.1", + "target": "simd_cesu8" + }, + { + "id": "thiserror 2.0.18", + "target": "thiserror" + } + ], + "selects": { + "cfg(windows)": [ + { + "id": "windows-link 0.2.1", + "target": "windows_link" + } + ] + } + }, + "edition": "2024", + "proc_macro_deps": { + "common": [ + { + "id": "jni-macros 0.22.4", + "target": "jni_macros" + } + ], + "selects": {} + }, + "version": "0.22.4" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "walkdir 2.5.0", + "target": "walkdir" + } + ], + "selects": {} + } + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": null + }, + "jni-macros 0.22.4": { + "name": "jni-macros", + "version": "0.22.4", + "package_url": "https://github.com/jni-rs/jni-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/jni-macros/0.22.4/download", + "sha256": "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "jni_macros", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "jni_macros", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { + "deps": { "common": [ - "std" + { + "id": "jni-macros 0.22.4", + "target": "build_script_build" + }, + { + "id": "proc-macro2 1.0.106", + "target": "proc_macro2" + }, + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "simd_cesu8 1.1.1", + "target": "simd_cesu8" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } ], "selects": {} }, - "edition": "2018", - "version": "2.12.0" + "edition": "2024", + "version": "0.22.4" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "rustc_version 0.4.1", + "target": "rustc_version" + } + ], + "selects": {} + } }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": null }, - "itertools 0.13.0": { - "name": "itertools", - "version": "0.13.0", - "package_url": "https://github.com/rust-itertools/itertools", + "jni-sys 0.4.1": { + "name": "jni-sys", + "version": "0.4.1", + "package_url": "https://github.com/jni-rs/jni-sys", "repository": { "Http": { - "url": "https://static.crates.io/crates/itertools/0.13.0/download", - "sha256": "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" + "url": "https://static.crates.io/crates/jni-sys/0.4.1/download", + "sha256": "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" } }, "targets": [ { "Library": { - "crate_name": "itertools", + "crate_name": "jni_sys", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -3904,22 +4565,22 @@ } } ], - "library_target_name": "itertools", + "library_target_name": "jni_sys", "common_attrs": { "compile_data_glob": [ "**" ], - "deps": { + "edition": "2021", + "proc_macro_deps": { "common": [ { - "id": "either 1.15.0", - "target": "either" + "id": "jni-sys-macros 0.4.1", + "target": "jni_sys_macros" } ], "selects": {} }, - "edition": "2018", - "version": "0.13.0" + "version": "0.4.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -3928,20 +4589,20 @@ ], "license_file": "LICENSE-APACHE" }, - "itoa 1.0.18": { - "name": "itoa", - "version": "1.0.18", - "package_url": "https://github.com/dtolnay/itoa", + "jni-sys-macros 0.4.1": { + "name": "jni-sys-macros", + "version": "0.4.1", + "package_url": "https://github.com/jni-rs/jni-sys", "repository": { "Http": { - "url": "https://static.crates.io/crates/itoa/1.0.18/download", - "sha256": "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + "url": "https://static.crates.io/crates/jni-sys-macros/0.4.1/download", + "sha256": "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" } }, "targets": [ { - "Library": { - "crate_name": "itoa", + "ProcMacro": { + "crate_name": "jni_sys_macros", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -3952,20 +4613,33 @@ } } ], - "library_target_name": "itoa", + "library_target_name": "jni_sys_macros", "common_attrs": { "compile_data_glob": [ "**" ], + "deps": { + "common": [ + { + "id": "quote 1.0.45", + "target": "quote" + }, + { + "id": "syn 2.0.117", + "target": "syn" + } + ], + "selects": {} + }, "edition": "2021", - "version": "1.0.18" + "version": "0.4.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-APACHE" + "license_file": null }, "js-sys 0.3.91": { "name": "js-sys", @@ -4209,6 +4883,45 @@ ], "license_file": "LICENSE" }, + "linked-hash-map 0.5.6": { + "name": "linked-hash-map", + "version": "0.5.6", + "package_url": "https://github.com/contain-rs/linked-hash-map", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/linked-hash-map/0.5.6/download", + "sha256": "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + } + }, + "targets": [ + { + "Library": { + "crate_name": "linked_hash_map", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "linked_hash_map", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2015", + "version": "0.5.6" + }, + "license": "MIT/Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "litemap 0.8.1": { "name": "litemap", "version": "0.8.1", @@ -4341,6 +5054,54 @@ ], "license_file": "LICENSE-APACHE" }, + "lru-cache 0.1.2": { + "name": "lru-cache", + "version": "0.1.2", + "package_url": "https://github.com/contain-rs/lru-cache", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/lru-cache/0.1.2/download", + "sha256": "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" + } + }, + "targets": [ + { + "Library": { + "crate_name": "lru_cache", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "lru_cache", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "linked-hash-map 0.5.6", + "target": "linked_hash_map" + } + ], + "selects": {} + }, + "edition": "2015", + "version": "0.1.2" + }, + "license": "MIT/Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "memchr 2.8.0": { "name": "memchr", "version": "2.8.0", @@ -4758,6 +5519,45 @@ ], "license_file": "LICENSE-APACHE" }, + "ndk-context 0.1.1": { + "name": "ndk-context", + "version": "0.1.1", + "package_url": "https://github.com/rust-windowing/android-ndk-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/ndk-context/0.1.1/download", + "sha256": "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "ndk_context", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "ndk_context", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2021", + "version": "0.1.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": null + }, "nom 7.1.3": { "name": "nom", "version": "7.1.3", @@ -4845,15 +5645,102 @@ "compile_data_glob": [ "**" ], - "edition": "2021", - "version": "0.2.1" + "edition": "2021", + "version": "0.2.1" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-Apache" + }, + "num-traits 0.2.19": { + "name": "num-traits", + "version": "0.2.19", + "package_url": "https://github.com/rust-num/num-traits", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/num-traits/0.2.19/download", + "sha256": "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" + } + }, + "targets": [ + { + "Library": { + "crate_name": "num_traits", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "num_traits", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "num-traits 0.2.19", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.2.19" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "autocfg 1.5.0", + "target": "autocfg" + } + ], + "selects": {} + } }, "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" ], - "license_file": "LICENSE-Apache" + "license_file": "LICENSE-APACHE" }, "once_cell 1.21.4": { "name": "once_cell", @@ -5329,61 +6216,6 @@ ], "license_file": "LICENSE-Apache" }, - "ppv-lite86 0.2.21": { - "name": "ppv-lite86", - "version": "0.2.21", - "package_url": "https://github.com/cryptocorrosion/cryptocorrosion", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/ppv-lite86/0.2.21/download", - "sha256": "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" - } - }, - "targets": [ - { - "Library": { - "crate_name": "ppv_lite86", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "ppv_lite86", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "simd", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "zerocopy 0.8.47", - "target": "zerocopy" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.2.21" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, "predicates 3.1.4": { "name": "predicates", "version": "3.1.4", @@ -5527,6 +6359,69 @@ ], "license_file": "LICENSE-APACHE" }, + "prefix-trie 0.8.3": { + "name": "prefix-trie", + "version": "0.8.3", + "package_url": "https://github.com/tiborschneider/prefix-trie", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/prefix-trie/0.8.3/download", + "sha256": "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" + } + }, + "targets": [ + { + "Library": { + "crate_name": "prefix_trie", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "prefix_trie", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "ipnet" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "either 1.15.0", + "target": "either" + }, + { + "id": "ipnet 2.12.0", + "target": "ipnet" + }, + { + "id": "num-traits 0.2.19", + "target": "num_traits" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.8.3" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "prettyplease 0.2.37": { "name": "prettyplease", "version": "0.2.37", @@ -5777,46 +6672,6 @@ ], "license_file": "LICENSE-APACHE" }, - "r-efi 5.3.0": { - "name": "r-efi", - "version": "5.3.0", - "package_url": "https://github.com/r-efi/r-efi", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/r-efi/5.3.0/download", - "sha256": "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - } - }, - "targets": [ - { - "Library": { - "crate_name": "r_efi", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "r_efi", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "edition": "2018", - "version": "5.3.0" - }, - "license": "MIT OR Apache-2.0 OR LGPL-2.1-or-later", - "license_ids": [ - "Apache-2.0", - "LGPL-2.1", - "MIT" - ], - "license_file": null - }, "r-efi 6.0.0": { "name": "r-efi", "version": "6.0.0", @@ -5857,14 +6712,14 @@ ], "license_file": null }, - "rand 0.9.2": { + "rand 0.10.1": { "name": "rand", - "version": "0.9.2", + "version": "0.10.1", "package_url": "https://github.com/rust-random/rand", "repository": { "Http": { - "url": "https://static.crates.io/crates/rand/0.9.2/download", - "sha256": "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" + "url": "https://static.crates.io/crates/rand/0.10.1/download", + "sha256": "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" } }, "targets": [ @@ -5889,9 +6744,9 @@ "crate_features": { "common": [ "alloc", - "os_rng", "std", "std_rng", + "sys_rng", "thread_rng" ], "selects": {} @@ -5899,76 +6754,22 @@ "deps": { "common": [ { - "id": "rand_chacha 0.9.0", - "target": "rand_chacha" + "id": "chacha20 0.10.0", + "target": "chacha20" }, { - "id": "rand_core 0.9.5", - "target": "rand_core" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.9.2" - }, - "license": "MIT OR Apache-2.0", - "license_ids": [ - "Apache-2.0", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "rand_chacha 0.9.0": { - "name": "rand_chacha", - "version": "0.9.0", - "package_url": "https://github.com/rust-random/rand", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/rand_chacha/0.9.0/download", - "sha256": "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" - } - }, - "targets": [ - { - "Library": { - "crate_name": "rand_chacha", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "rand_chacha", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "ppv-lite86 0.2.21", - "target": "ppv_lite86" + "id": "getrandom 0.4.2", + "target": "getrandom" }, { - "id": "rand_core 0.9.5", + "id": "rand_core 0.10.1", "target": "rand_core" } ], "selects": {} }, - "edition": "2021", - "version": "0.9.0" + "edition": "2024", + "version": "0.10.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -5977,14 +6778,14 @@ ], "license_file": "LICENSE-APACHE" }, - "rand_core 0.9.5": { + "rand_core 0.10.1": { "name": "rand_core", - "version": "0.9.5", - "package_url": "https://github.com/rust-random/rand", + "version": "0.10.1", + "package_url": "https://github.com/rust-random/rand_core", "repository": { "Http": { - "url": "https://static.crates.io/crates/rand_core/0.9.5/download", - "sha256": "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + "url": "https://static.crates.io/crates/rand_core/0.10.1/download", + "sha256": "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" } }, "targets": [ @@ -6006,24 +6807,8 @@ "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "os_rng", - "std" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "getrandom 0.3.4", - "target": "getrandom" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.9.5" + "edition": "2024", + "version": "0.10.1" }, "license": "MIT OR Apache-2.0", "license_ids": [ @@ -6417,14 +7202,60 @@ "package_url": "https://github.com/rust-lang-nursery/rustc-hash", "repository": { "Http": { - "url": "https://static.crates.io/crates/rustc-hash/1.1.0/download", - "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + "url": "https://static.crates.io/crates/rustc-hash/1.1.0/download", + "sha256": "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + } + }, + "targets": [ + { + "Library": { + "crate_name": "rustc_hash", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "rustc_hash", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "edition": "2015", + "version": "1.1.0" + }, + "license": "Apache-2.0/MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "rustc_version 0.4.1": { + "name": "rustc_version", + "version": "0.4.1", + "package_url": "https://github.com/djc/rustc-version-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/rustc_version/0.4.1/download", + "sha256": "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" } }, "targets": [ { "Library": { - "crate_name": "rustc_hash", + "crate_name": "rustc_version", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -6435,22 +7266,24 @@ } } ], - "library_target_name": "rustc_hash", + "library_target_name": "rustc_version", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { + "deps": { "common": [ - "default", - "std" + { + "id": "semver 1.0.27", + "target": "semver" + } ], "selects": {} }, - "edition": "2015", - "version": "1.1.0" + "edition": "2018", + "version": "0.4.1" }, - "license": "Apache-2.0/MIT", + "license": "MIT OR Apache-2.0", "license_ids": [ "Apache-2.0", "MIT" @@ -6532,7 +7365,7 @@ "alias": "pki_types" }, { - "id": "rustls-webpki 0.103.10", + "id": "rustls-webpki 0.103.13", "target": "webpki" }, { @@ -6633,14 +7466,14 @@ ], "license_file": "LICENSE-APACHE" }, - "rustls-webpki 0.103.10": { + "rustls-webpki 0.103.13": { "name": "rustls-webpki", - "version": "0.103.10", + "version": "0.103.13", "package_url": "https://github.com/rustls/webpki", "repository": { "Http": { - "url": "https://static.crates.io/crates/rustls-webpki/0.103.10/download", - "sha256": "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" + "url": "https://static.crates.io/crates/rustls-webpki/0.103.13/download", + "sha256": "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" } }, "targets": [ @@ -6689,7 +7522,7 @@ "selects": {} }, "edition": "2021", - "version": "0.103.10" + "version": "0.103.13" }, "license": "ISC", "license_ids": [ @@ -6768,6 +7601,56 @@ ], "license_file": "LICENSE-APACHE" }, + "same-file 1.0.6": { + "name": "same-file", + "version": "1.0.6", + "package_url": "https://github.com/BurntSushi/same-file", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/same-file/1.0.6/download", + "sha256": "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" + } + }, + "targets": [ + { + "Library": { + "crate_name": "same_file", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "same_file", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [], + "selects": { + "cfg(windows)": [ + { + "id": "winapi-util 0.1.11", + "target": "winapi_util" + } + ] + } + }, + "edition": "2018", + "version": "1.0.6" + }, + "license": "Unlicense/MIT", + "license_ids": [ + "MIT", + "Unlicense" + ], + "license_file": "LICENSE-MIT" + }, "scopeguard 1.2.0": { "name": "scopeguard", "version": "1.2.0", @@ -6836,6 +7719,13 @@ "compile_data_glob": [ "**" ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, "edition": "2018", "version": "1.0.27" }, @@ -7269,6 +8159,106 @@ ], "license_file": "LICENSE-APACHE" }, + "simd_cesu8 1.1.1": { + "name": "simd_cesu8", + "version": "1.1.1", + "package_url": "https://github.com/seancroach/simd_cesu8", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/simd_cesu8/1.1.1/download", + "sha256": "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" + } + }, + "targets": [ + { + "Library": { + "crate_name": "simd_cesu8", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "simd_cesu8", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "default", + "std" + ], + "selects": {} + }, + "deps": { + "common": [ + { + "id": "simdutf8 0.1.5", + "target": "simdutf8" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "1.1.1" + }, + "license": "Apache-2.0 OR MIT", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "simdutf8 0.1.5": { + "name": "simdutf8", + "version": "0.1.5", + "package_url": "https://github.com/rusticstuff/simdutf8", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/simdutf8/0.1.5/download", + "sha256": "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + } + }, + "targets": [ + { + "Library": { + "crate_name": "simdutf8", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "simdutf8", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "crate_features": { + "common": [ + "std" + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.5" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-Apache" + }, "slab 0.4.12": { "name": "slab", "version": "0.4.12", @@ -7626,11 +8616,146 @@ "edition": "2018", "version": "0.13.2" }, - "license": "MIT", + "license": "MIT", + "license_ids": [ + "MIT" + ], + "license_file": "LICENSE" + }, + "system-configuration 0.7.0": { + "name": "system-configuration", + "version": "0.7.0", + "package_url": "https://github.com/mullvad/system-configuration-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/system-configuration/0.7.0/download", + "sha256": "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "system_configuration", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "system_configuration", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "bitflags 2.11.0", + "target": "bitflags" + }, + { + "id": "core-foundation 0.9.4", + "target": "core_foundation" + }, + { + "id": "system-configuration-sys 0.6.0", + "target": "system_configuration_sys" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.7.0" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "system-configuration-sys 0.6.0": { + "name": "system-configuration-sys", + "version": "0.6.0", + "package_url": "https://github.com/mullvad/system-configuration-rs", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/system-configuration-sys/0.6.0/download", + "sha256": "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" + } + }, + "targets": [ + { + "Library": { + "crate_name": "system_configuration_sys", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + }, + { + "BuildScript": { + "crate_name": "build_script_build", + "crate_root": "build.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "system_configuration_sys", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "core-foundation-sys 0.8.7", + "target": "core_foundation_sys" + }, + { + "id": "libc 0.2.183", + "target": "libc" + }, + { + "id": "system-configuration-sys 0.6.0", + "target": "build_script_build" + } + ], + "selects": {} + }, + "edition": "2021", + "version": "0.6.0" + }, + "build_script_attrs": { + "compile_data_glob": [ + "**" + ], + "compile_data_glob_excludes": [ + "**/*.rs" + ], + "data_glob": [ + "**" + ] + }, + "license": "MIT OR Apache-2.0", "license_ids": [ + "Apache-2.0", "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, "tagptr 0.2.0": { "name": "tagptr", @@ -8884,6 +10009,61 @@ ], "license_file": "LICENSE-APACHE" }, + "walkdir 2.5.0": { + "name": "walkdir", + "version": "2.5.0", + "package_url": "https://github.com/BurntSushi/walkdir", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/walkdir/2.5.0/download", + "sha256": "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" + } + }, + "targets": [ + { + "Library": { + "crate_name": "walkdir", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "walkdir", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "same-file 1.0.6", + "target": "same_file" + } + ], + "selects": { + "cfg(windows)": [ + { + "id": "winapi-util 0.1.11", + "target": "winapi_util" + } + ] + } + }, + "edition": "2018", + "version": "2.5.0" + }, + "license": "Unlicense/MIT", + "license_ids": [ + "MIT", + "Unlicense" + ], + "license_file": "LICENSE-MIT" + }, "wasi 0.11.1+wasi-snapshot-preview1": { "name": "wasi", "version": "0.11.1+wasi-snapshot-preview1", @@ -9520,14 +10700,14 @@ ], "license_file": null }, - "webpki-roots 0.26.11": { + "webpki-roots 1.0.6": { "name": "webpki-roots", - "version": "0.26.11", + "version": "1.0.6", "package_url": "https://github.com/rustls/webpki-roots", "repository": { "Http": { - "url": "https://static.crates.io/crates/webpki-roots/0.26.11/download", - "sha256": "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" + "url": "https://static.crates.io/crates/webpki-roots/1.0.6/download", + "sha256": "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" } }, "targets": [ @@ -9552,15 +10732,15 @@ "deps": { "common": [ { - "id": "webpki-roots 1.0.6", - "target": "webpki_roots", - "alias": "parent" + "id": "rustls-pki-types 1.14.0", + "target": "rustls_pki_types", + "alias": "pki_types" } ], "selects": {} }, "edition": "2021", - "version": "0.26.11" + "version": "1.0.6" }, "license": "CDLA-Permissive-2.0", "license_ids": [ @@ -9568,20 +10748,20 @@ ], "license_file": "LICENSE" }, - "webpki-roots 1.0.6": { - "name": "webpki-roots", - "version": "1.0.6", - "package_url": "https://github.com/rustls/webpki-roots", + "widestring 1.2.1": { + "name": "widestring", + "version": "1.2.1", + "package_url": "https://github.com/VoidStarKat/widestring-rs", "repository": { "Http": { - "url": "https://static.crates.io/crates/webpki-roots/1.0.6/download", - "sha256": "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" + "url": "https://static.crates.io/crates/widestring/1.2.1/download", + "sha256": "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" } }, "targets": [ { "Library": { - "crate_name": "webpki_roots", + "crate_name": "widestring", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -9592,44 +10772,43 @@ } } ], - "library_target_name": "webpki_roots", + "library_target_name": "widestring", "common_attrs": { "compile_data_glob": [ "**" ], - "deps": { + "crate_features": { "common": [ - { - "id": "rustls-pki-types 1.14.0", - "target": "rustls_pki_types", - "alias": "pki_types" - } + "alloc", + "default", + "std" ], "selects": {} }, "edition": "2021", - "version": "1.0.6" + "version": "1.2.1" }, - "license": "CDLA-Permissive-2.0", + "license": "MIT OR Apache-2.0", "license_ids": [ - "CDLA-Permissive-2.0" + "Apache-2.0", + "MIT" ], - "license_file": "LICENSE" + "license_file": "LICENSE-APACHE" }, - "widestring 1.2.1": { - "name": "widestring", - "version": "1.2.1", - "package_url": "https://github.com/VoidStarKat/widestring-rs", + "winapi-util 0.1.11": { + "name": "winapi-util", + "version": "0.1.11", + "package_url": "https://github.com/BurntSushi/winapi-util", "repository": { "Http": { - "url": "https://static.crates.io/crates/widestring/1.2.1/download", - "sha256": "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + "url": "https://static.crates.io/crates/winapi-util/0.1.11/download", + "sha256": "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" } }, "targets": [ { "Library": { - "crate_name": "widestring", + "crate_name": "winapi_util", "crate_root": "src/lib.rs", "srcs": { "allow_empty": true, @@ -9640,28 +10819,31 @@ } } ], - "library_target_name": "widestring", + "library_target_name": "winapi_util", "common_attrs": { "compile_data_glob": [ "**" ], - "crate_features": { - "common": [ - "alloc", - "default", - "std" - ], - "selects": {} + "deps": { + "common": [], + "selects": { + "cfg(windows)": [ + { + "id": "windows-sys 0.61.2", + "target": "windows_sys" + } + ] + } }, "edition": "2021", - "version": "1.2.1" + "version": "0.1.11" }, - "license": "MIT OR Apache-2.0", + "license": "Unlicense OR MIT", "license_ids": [ - "Apache-2.0", - "MIT" + "MIT", + "Unlicense" ], - "license_file": "LICENSE-APACHE" + "license_file": "LICENSE-MIT" }, "windows-link 0.2.1": { "name": "windows-link", @@ -11358,152 +12540,6 @@ ], "license_file": "LICENSE" }, - "zerocopy 0.8.47": { - "name": "zerocopy", - "version": "0.8.47", - "package_url": "https://github.com/google/zerocopy", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/zerocopy/0.8.47/download", - "sha256": "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" - } - }, - "targets": [ - { - "Library": { - "crate_name": "zerocopy", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - }, - { - "BuildScript": { - "crate_name": "build_script_build", - "crate_root": "build.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "zerocopy", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "crate_features": { - "common": [ - "simd" - ], - "selects": {} - }, - "deps": { - "common": [ - { - "id": "zerocopy 0.8.47", - "target": "build_script_build" - } - ], - "selects": {} - }, - "edition": "2021", - "proc_macro_deps": { - "common": [], - "selects": { - "cfg(any())": [ - { - "id": "zerocopy-derive 0.8.47", - "target": "zerocopy_derive" - } - ] - } - }, - "version": "0.8.47" - }, - "build_script_attrs": { - "compile_data_glob": [ - "**" - ], - "compile_data_glob_excludes": [ - "**/*.rs" - ], - "data_glob": [ - "**" - ] - }, - "license": "BSD-2-Clause OR Apache-2.0 OR MIT", - "license_ids": [ - "Apache-2.0", - "BSD-2-Clause", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, - "zerocopy-derive 0.8.47": { - "name": "zerocopy-derive", - "version": "0.8.47", - "package_url": "https://github.com/google/zerocopy", - "repository": { - "Http": { - "url": "https://static.crates.io/crates/zerocopy-derive/0.8.47/download", - "sha256": "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" - } - }, - "targets": [ - { - "ProcMacro": { - "crate_name": "zerocopy_derive", - "crate_root": "src/lib.rs", - "srcs": { - "allow_empty": true, - "include": [ - "**/*.rs" - ] - } - } - } - ], - "library_target_name": "zerocopy_derive", - "common_attrs": { - "compile_data_glob": [ - "**" - ], - "deps": { - "common": [ - { - "id": "proc-macro2 1.0.106", - "target": "proc_macro2" - }, - { - "id": "quote 1.0.45", - "target": "quote" - }, - { - "id": "syn 2.0.117", - "target": "syn" - } - ], - "selects": {} - }, - "edition": "2021", - "version": "0.8.47" - }, - "license": "BSD-2-Clause OR Apache-2.0 OR MIT", - "license_ids": [ - "Apache-2.0", - "BSD-2-Clause", - "MIT" - ], - "license_file": "LICENSE-APACHE" - }, "zerofrom 0.1.6": { "name": "zerofrom", "version": "0.1.6", @@ -11952,6 +12988,14 @@ "x86_64-unknown-nixos-gnu" ], "cfg(all(target_arch = \"aarch64\", target_env = \"msvc\", not(windows_raw_dylib)))": [], + "cfg(all(target_arch = \"aarch64\", target_os = \"android\"))": [], + "cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))": [ + "aarch64-unknown-linux-gnu" + ], + "cfg(all(target_arch = \"aarch64\", target_vendor = \"apple\"))": [ + "aarch64-apple-darwin" + ], + "cfg(all(target_arch = \"loongarch64\", target_os = \"linux\"))": [], "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p2\"))": [], "cfg(all(target_arch = \"wasm32\", target_os = \"wasi\", target_env = \"p3\"))": [], "cfg(all(target_arch = \"x86\", target_env = \"gnu\", not(target_abi = \"llvm\"), not(windows_raw_dylib)))": [], @@ -11962,6 +13006,11 @@ ], "cfg(all(target_os = \"uefi\", getrandom_backend = \"efi_rng\"))": [], "cfg(any())": [], + "cfg(any(target_arch = \"x86_64\", target_arch = \"x86\"))": [ + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-nixos-gnu" + ], "cfg(any(target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"hurd\", target_os = \"illumos\", target_os = \"cygwin\", all(target_os = \"horizon\", target_arch = \"arm\")))": [], "cfg(any(target_os = \"haiku\", target_os = \"redox\", target_os = \"nto\", target_os = \"aix\"))": [], "cfg(any(target_os = \"ios\", target_os = \"visionos\", target_os = \"watchos\", target_os = \"tvos\"))": [], @@ -11975,6 +13024,7 @@ "x86_64-unknown-linux-gnu", "x86_64-unknown-nixos-gnu" ], + "cfg(target_os = \"android\")": [], "cfg(target_os = \"hermit\")": [], "cfg(target_os = \"netbsd\")": [], "cfg(target_os = \"redox\")": [], @@ -12013,7 +13063,7 @@ }, "direct_deps": [ "bindgen 0.70.1", - "hickory-resolver 0.25.2", + "hickory-resolver 0.26.1", "mockall 0.13.1", "serde 1.0.228", "serde_json 1.0.149", diff --git a/docs/bazel/deps.yaml b/docs/bazel/deps.yaml index 92d39203e44fe..32e3ec6c66ad8 100644 --- a/docs/bazel/deps.yaml +++ b/docs/bazel/deps.yaml @@ -2,7 +2,7 @@ envoy_examples: project_name: "envoy-examples" project_desc: "Envoy proxy examples" project_url: "https://github.com/envoyproxy/examples" - release_date: "2026-04-02" + release_date: "2026-04-20" use_category: - test_only license: "Apache-2.0" diff --git a/docs/bazel/repository_locations.bzl b/docs/bazel/repository_locations.bzl index 96c371524dca4..bc9b85003645a 100644 --- a/docs/bazel/repository_locations.bzl +++ b/docs/bazel/repository_locations.bzl @@ -1,7 +1,7 @@ REPOSITORY_LOCATIONS_SPEC = dict( envoy_examples = dict( - version = "0.2.2", - sha256 = "865726ea8c02dc7418f75d6aecf17ccb61ac4fad7a6a73649220e7520f6d0f8d", + version = "0.2.3", + sha256 = "a0fc7991a346333c1e3d17ab6d8a68bc3d0a4e76a6f6adf83b51eab9e7103bd8", strip_prefix = "examples-{version}", urls = ["https://github.com/envoyproxy/examples/archive/v{version}.tar.gz"], ), diff --git a/docs/conf.py b/docs/conf.py index 40099cb9d648a..e5981a0f9ebeb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -145,7 +145,7 @@ def _config(key): 'sphinx.ext.extlinks', 'sphinx.ext.ifconfig', 'sphinx.ext.intersphinx', - 'envoy.docs.sphinx_runner.sphinx_tabs.tabs', + 'sphinx_tabs.tabs', 'sphinx_copybutton', 'envoy.docs.sphinx_runner.ext.validating_code_block', 'sphinxext.rediraffe', diff --git a/docs/inventories/v1.35/objects.inv b/docs/inventories/v1.35/objects.inv index e83806f403ab3..7b81b398df5d4 100644 Binary files a/docs/inventories/v1.35/objects.inv and b/docs/inventories/v1.35/objects.inv differ diff --git a/docs/inventories/v1.36/objects.inv b/docs/inventories/v1.36/objects.inv index cd174d5d534d3..e533821741e03 100644 Binary files a/docs/inventories/v1.36/objects.inv and b/docs/inventories/v1.36/objects.inv differ diff --git a/docs/inventories/v1.37/objects.inv b/docs/inventories/v1.37/objects.inv index a8c65223403e4..f8436913d7c1a 100644 Binary files a/docs/inventories/v1.37/objects.inv and b/docs/inventories/v1.37/objects.inv differ diff --git a/docs/inventories/v1.38/objects.inv b/docs/inventories/v1.38/objects.inv new file mode 100644 index 0000000000000..00ae953edfc6d Binary files /dev/null and b/docs/inventories/v1.38/objects.inv differ diff --git a/docs/root/_configs/reverse_connection/initiator-envoy.yaml b/docs/root/_configs/reverse_connection/initiator-envoy.yaml index 495aab0526b64..a9d47176796f7 100644 --- a/docs/root/_configs/reverse_connection/initiator-envoy.yaml +++ b/docs/root/_configs/reverse_connection/initiator-envoy.yaml @@ -10,6 +10,22 @@ bootstrap_extensions: "@type": >- type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.downstream_socket_interface.v3.DownstreamReverseConnectionSocketInterface stat_prefix: "downstream_reverse_connection" + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": >- + type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + log_format: + text_format_source: + inline_string: >- + [%START_TIME%] reverse_tunnel_initiator + event=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:event)% + node=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:node_id)% + cluster=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:cluster_id)% + tenant=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:tenant_id)% + upstream_cluster=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:upstream_cluster)% + host=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:host_address)% + error=%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:error)% static_resources: listeners: @@ -61,7 +77,7 @@ static_resources: - endpoint: address: socket_address: - address: upstream-envoy # Address of upstream-envoy + address: 127.0.0.1 # Address of upstream-envoy port_value: 9000 # Port for rev_conn_api_listener # Backend HTTP service behind downstream which @@ -76,5 +92,5 @@ static_resources: - endpoint: address: socket_address: - address: downstream-service - port_value: 80 + address: 127.0.0.1 + port_value: 8080 diff --git a/docs/root/_configs/reverse_connection/responder-envoy.yaml b/docs/root/_configs/reverse_connection/responder-envoy.yaml index 544ae5689e2b5..c7d1f948c3a7c 100644 --- a/docs/root/_configs/reverse_connection/responder-envoy.yaml +++ b/docs/root/_configs/reverse_connection/responder-envoy.yaml @@ -10,6 +10,18 @@ bootstrap_extensions: "@type": >- type.googleapis.com/envoy.extensions.bootstrap.reverse_tunnel.upstream_socket_interface.v3.UpstreamReverseConnectionSocketInterface stat_prefix: "upstream_reverse_connection" + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + log_format: + text_format_source: + inline_string: >- + event=%DYNAMIC_METADATA(envoy.reverse_tunnel.lifecycle:event)% + node=%FILTER_STATE(envoy.reverse_tunnel.node_id)% + cluster=%FILTER_STATE(envoy.reverse_tunnel.cluster_id)% + tenant=%FILTER_STATE(envoy.reverse_tunnel.tenant_id)% + reason=%CONNECTION_TERMINATION_DETAILS% static_resources: listeners: @@ -93,6 +105,10 @@ static_resources: - name: reverse_connection_cluster connect_timeout: 200s lb_policy: CLUSTER_PROVIDED + filters: + - name: envoy.filters.upstream_network.reverse_tunnel_lifecycle + typed_config: + "@type": type.googleapis.com/google.protobuf.Empty cluster_type: name: envoy.clusters.reverse_connection typed_config: diff --git a/docs/root/_include/ssl_stats.rst b/docs/root/_include/ssl_stats.rst index 4dbeddd4d0a70..93f9b247a67ee 100644 --- a/docs/root/_include/ssl_stats.rst +++ b/docs/root/_include/ssl_stats.rst @@ -18,4 +18,3 @@ curves., Counter, Total successful TLS connections that used ECDHE curve sigalgs., Counter, Total successful TLS connections that used signature algorithm versions., Counter, Total successful TLS connections that used protocol version - was_key_usage_invalid, Counter, Total successful TLS connections that used an `invalid keyUsage extension `_. diff --git a/docs/root/api-v3/bootstrap/bootstrap.rst b/docs/root/api-v3/bootstrap/bootstrap.rst index a5acf809ebec5..238e19d8bf03a 100644 --- a/docs/root/api-v3/bootstrap/bootstrap.rst +++ b/docs/root/api-v3/bootstrap/bootstrap.rst @@ -9,6 +9,7 @@ Bootstrap ../extensions/bootstrap/internal_listener/v3/internal_listener.proto ../extensions/bootstrap/reverse_tunnel/downstream_socket_interface/v3/downstream_reverse_connection_socket_interface.proto ../extensions/bootstrap/reverse_tunnel/upstream_socket_interface/v3/upstream_reverse_connection_socket_interface.proto + ../extensions/network/socket_interface/sockmap/v3/sockmap.proto ../config/metrics/v3/metrics_service.proto ../config/overload/v3/overload.proto ../config/ratelimit/v3/rls.proto diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index 94e0003c7fd05..752455e491676 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -36,6 +36,7 @@ Common messages ../config/common/mutation_rules/v3/mutation_rules.proto ../extensions/early_data/v3/default_early_data_policy.proto ../config/core/v3/http_uri.proto + ../type/v3/scope.proto ../extensions/matching/input_matchers/ip/v3/ip.proto ../extensions/matching/input_matchers/metadata/v3/metadata.proto ../extensions/matching/input_matchers/runtime_fraction/v3/runtime_fraction.proto diff --git a/docs/root/api-v3/config/contrib/contrib.rst b/docs/root/api-v3/config/contrib/contrib.rst index ac17d06076276..ecd7e9b9e1bb6 100644 --- a/docs/root/api-v3/config/contrib/contrib.rst +++ b/docs/root/api-v3/config/contrib/contrib.rst @@ -12,12 +12,13 @@ Contrib extensions cryptomb/cryptomb hyperscan/matcher hyperscan/regex_engine - dlb/dlb postgres/postgres qat/qat kae/kae http_tcp_bridge/http_tcp_bridge kafka_stats_sink/kafka_stats_sink + wasm_filter_stats_sink/wasm_filter_stats_sink tap_sinks/tap_sinks load_balancing_policies/peak_ewma/peak_ewma istio/istio + reverse_tunnel_reporter/reverse_tunnel_reporter diff --git a/docs/root/api-v3/config/contrib/dlb/dlb.rst b/docs/root/api-v3/config/contrib/dlb/dlb.rst deleted file mode 100644 index f3e295ab11ff1..0000000000000 --- a/docs/root/api-v3/config/contrib/dlb/dlb.rst +++ /dev/null @@ -1,8 +0,0 @@ -Dlb connection balancer -======================= - -.. toctree:: - :glob: - :maxdepth: 2 - - ../../../extensions/network/connection_balance/dlb/v3alpha/* diff --git a/docs/root/api-v3/config/contrib/reverse_tunnel_reporter/reverse_tunnel_reporter.rst b/docs/root/api-v3/config/contrib/reverse_tunnel_reporter/reverse_tunnel_reporter.rst new file mode 100644 index 0000000000000..6f174b730d66d --- /dev/null +++ b/docs/root/api-v3/config/contrib/reverse_tunnel_reporter/reverse_tunnel_reporter.rst @@ -0,0 +1,11 @@ +.. _extension_envoy.bootstrap.reverse_tunnel.reverse_tunnel_reporting_service: + +Reverse tunnel reporter +======================= + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../../extensions/reverse_tunnel_reporters/v3alpha/clients/grpc_client/* + ../../../extensions/reverse_tunnel_reporters/v3alpha/reporters/* diff --git a/docs/root/api-v3/config/contrib/wasm_filter_stats_sink/wasm_filter_stats_sink.rst b/docs/root/api-v3/config/contrib/wasm_filter_stats_sink/wasm_filter_stats_sink.rst new file mode 100644 index 0000000000000..63290dc803670 --- /dev/null +++ b/docs/root/api-v3/config/contrib/wasm_filter_stats_sink/wasm_filter_stats_sink.rst @@ -0,0 +1,8 @@ +Wasm Filter Stats Sink +====================== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../../extensions/stat_sinks/wasm_filter/v3/* diff --git a/docs/root/api-v3/config/stat_sinks/stat_sinks.rst b/docs/root/api-v3/config/stat_sinks/stat_sinks.rst index 6b3d7717741f6..a0154f0e8ba01 100644 --- a/docs/root/api-v3/config/stat_sinks/stat_sinks.rst +++ b/docs/root/api-v3/config/stat_sinks/stat_sinks.rst @@ -5,6 +5,7 @@ Stat sinks :glob: :maxdepth: 2 + ../../extensions/stat_sinks/dynamic_modules/v3/* ../../extensions/stat_sinks/graphite_statsd/v3/* ../../extensions/stat_sinks/open_telemetry/v3/* ../../extensions/stat_sinks/wasm/v3/* diff --git a/docs/root/api-v3/listeners/listeners.rst b/docs/root/api-v3/listeners/listeners.rst index 7c0a68a52be03..bcd9eef03c091 100644 --- a/docs/root/api-v3/listeners/listeners.rst +++ b/docs/root/api-v3/listeners/listeners.rst @@ -6,7 +6,6 @@ Listeners :maxdepth: 2 ../config/listener/v3/api_listener.proto - ../extensions/network/connection_balance/dlb/v3alpha/dlb.proto ../config/listener/v3/listener_components.proto ../config/listener/v3/listener.proto ../config/listener/v3/quic_config.proto diff --git a/docs/root/configuration/advanced/substitution_formatter.rst b/docs/root/configuration/advanced/substitution_formatter.rst index 89ad4f8b0ff06..739a1ce425d0e 100644 --- a/docs/root/configuration/advanced/substitution_formatter.rst +++ b/docs/root/configuration/advanced/substitution_formatter.rst @@ -232,7 +232,7 @@ Current supported substitution commands include: ``%UPSTREAM_WIRE_BYTES_SENT%`` HTTP - Total number of bytes sent to the upstream by the http stream. + Total number of bytes sent to the upstream by the HTTP stream. TCP Total number of bytes sent to the upstream by the tcp proxy. @@ -242,7 +242,7 @@ Current supported substitution commands include: ``%UPSTREAM_WIRE_BYTES_RECEIVED%`` HTTP - Total number of bytes received from the upstream by the http stream. + Total number of bytes received from the upstream by the HTTP stream. TCP Total number of bytes received from the upstream by the tcp proxy. @@ -252,7 +252,7 @@ Current supported substitution commands include: ``%UPSTREAM_HEADER_BYTES_SENT%`` HTTP - Number of header bytes sent to the upstream by the http stream. + Number of header bytes sent to the upstream by the HTTP stream. TCP Total number of HTTP header bytes sent to the upstream stream, for TCP tunneling flows. Not supported for non-tunneling. @@ -262,14 +262,14 @@ Current supported substitution commands include: ``%UPSTREAM_DECOMPRESSED_HEADER_BYTES_SENT%`` HTTP - Number of decompressed header bytes sent to the upstream by the http stream. + Number of decompressed header bytes sent to the upstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. ``%UPSTREAM_HEADER_BYTES_RECEIVED%`` HTTP - Number of header bytes received from the upstream by the http stream. + Number of header bytes received from the upstream by the HTTP stream. TCP Total number of HTTP header bytes received from the upstream stream, for TCP tunneling flows. Not supported for non-tunneling. @@ -279,7 +279,7 @@ Current supported substitution commands include: ``%UPSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED%`` HTTP - Number of decompressed header bytes received from the upstream by the http stream. + Number of decompressed header bytes received from the upstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. @@ -287,7 +287,7 @@ Current supported substitution commands include: ``%DOWNSTREAM_WIRE_BYTES_SENT%`` HTTP - Total number of bytes sent to the downstream by the http stream. + Total number of bytes sent to the downstream by the HTTP stream. TCP Total number of bytes sent to the downstream by the tcp proxy. @@ -297,7 +297,7 @@ Current supported substitution commands include: ``%DOWNSTREAM_WIRE_BYTES_RECEIVED%`` HTTP - Total number of bytes received from the downstream by the http stream. Envoy over counts sizes of received HTTP/1.1 pipelined requests by adding up bytes of requests in the pipeline to the one currently being processed. + Total number of bytes received from the downstream by the HTTP stream. Envoy over counts sizes of received HTTP/1.1 pipelined requests by adding up bytes of requests in the pipeline to the one currently being processed. TCP Total number of bytes received from the downstream by the tcp proxy. @@ -307,21 +307,21 @@ Current supported substitution commands include: ``%DOWNSTREAM_HEADER_BYTES_SENT%`` HTTP - Number of header bytes sent to the downstream by the http stream. + Number of header bytes sent to the downstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. ``%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_SENT%`` HTTP - Number of decompressed header bytes sent to the downstream by the http stream. + Number of decompressed header bytes sent to the downstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. ``%DOWNSTREAM_HEADER_BYTES_RECEIVED%`` HTTP - Number of header bytes received from the downstream by the http stream. + Number of header bytes received from the downstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. @@ -330,7 +330,7 @@ Current supported substitution commands include: ``%DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED%`` HTTP - Number of decompressed header bytes received from the downstream by the http stream. + Number of decompressed header bytes received from the downstream by the HTTP stream. TCP/UDP Not implemented. It will appear as ``0`` in the access logs. @@ -357,6 +357,8 @@ Current supported substitution commands include: The ``START`` and ``END`` time points are specified by the following values (all values here are case-sensitive): + * ``DS_CX_BEG``: The time point of the downstream connection begin. + * ``DS_CX_END``: The time point of the downstream connection end. * ``DS_RX_BEG``: The time point of the downstream request receiving begin. * ``DS_RX_END``: The time point of the downstream request receiving end. * ``US_CX_BEG``: The time point of the upstream TCP connect begin. @@ -376,6 +378,12 @@ Current supported substitution commands include: Upstream connection establishment time points (``US_CX_*``, ``US_HS_END``) repeat for all requests in a given connection. + .. note:: + + The ``DS_CX_BEG`` time point reflects the downstream connection begin and repeats for all + requests on a given connection. The ``DS_CX_END`` time point is only populated for requests + that are active when the downstream connection closes and renders as ``"-"`` otherwise. + The ``PRECISION`` is specified by the following values (all values here are case-sensitive): * ``ms``: Millisecond precision. @@ -388,7 +396,19 @@ Current supported substitution commands include: ``*_TX_END`` values lower than ``*_RX_END`` values, in cases where upstream peer has half-closed its stream before downstream peer. In these cases the ``COMMON_DURATION`` value will become negative. - TCP/UDP + TCP + Total duration between the ``START`` time point and the ``END`` time point in specific ``PRECISION``. + The connection time points are populated for TCP connections and specified by the following + values (all values here are case-sensitive): + + * ``DS_CX_BEG``: The time point of the downstream connection begin. + * ``DS_CX_END``: The time point of the downstream connection end. + * ``US_CX_BEG``: The time point of the upstream TCP connect begin. + * ``US_CX_END``: The time point of the upstream TCP connect end. + + The ``PRECISION`` is the same as the HTTP case above. + + UDP Not implemented. It will appear as ``"-"`` in the access logs. ``%REQUEST_DURATION%`` @@ -1304,6 +1324,12 @@ Current supported substitution commands include: UDP Not implemented. It will appear as ``"-"`` in the access logs. +``%DOWNSTREAM_TLS_GROUP%`` + HTTP/TCP/THRIFT + The name of the TLS group used for the key agreement to establish the downstream TLS connection. + UDP + Not implemented. It will appear as ``"-"`` in the access logs. + ``%DOWNSTREAM_TLS_VERSION%`` HTTP/TCP/THRIFT The TLS version (e.g., ``TLSv1.2``, ``TLSv1.3``) used to establish the downstream TLS connection. @@ -1446,12 +1472,26 @@ Current supported substitution commands include: UDP Not implemented. It will appear as ``"-"`` in the access logs. +``%UPSTREAM_TLS_GROUP%`` + HTTP/TCP/THRIFT + The name of the TLS group used for the key agreement to establish the upstream TLS connection. + UDP + Not implemented. It will appear as ``"-"`` in the access logs. + ``%UPSTREAM_TLS_VERSION%`` HTTP/TCP/THRIFT The TLS version (e.g., ``TLSv1.2``, ``TLSv1.3``) used to establish the upstream TLS connection. UDP Not implemented. It will appear as ``"-"`` in the access logs. +.. _config_access_log_format_upstream_server_name: + +``%UPSTREAM_SERVER_NAME%`` + HTTP/TCP/THRIFT + The TLS SNI value used to establish the upstream TLS connection. + UDP + Not implemented. It will appear as ``"-"`` in the access logs. + ``%UPSTREAM_PEER_CERT%`` HTTP/TCP/THRIFT The server certificate in the URL-encoded PEM format used to establish the upstream TLS connection. diff --git a/docs/root/configuration/advanced/well_known_dynamic_metadata.rst b/docs/root/configuration/advanced/well_known_dynamic_metadata.rst index ecf1bdedb1b12..6b8d48af754f4 100644 --- a/docs/root/configuration/advanced/well_known_dynamic_metadata.rst +++ b/docs/root/configuration/advanced/well_known_dynamic_metadata.rst @@ -39,8 +39,8 @@ The :ref:`MCP filter ` emits the following dynamic meta :header: Name, Type, Description :widths: 1, 1, 4 - method, string, "The JSON-RPC method name (e.g., ``initialize``, ``tools/list``, ``tools/call``)." - id, number, "The JSON-RPC request ID." + method, string, "The JSON-RPC method name (e.g., ``initialize``, ``tools/list``, ``tools/call``). For JSON-RPC responses (containing ``result`` or ``error``), this is set to ``__jsonrpc_response``." + id, number/string, "The JSON-RPC request ID. Numeric for standard requests; string for server response routing (e.g., ``time__42``)." params, struct, "The params object from the JSON-RPC request containing method-specific parameters." This metadata is consumed internally by the :ref:`MCP router filter ` for diff --git a/docs/root/configuration/http/cluster_specifier/golang.rst b/docs/root/configuration/http/cluster_specifier/golang.rst index efe9e1e984ff1..2659858003725 100644 --- a/docs/root/configuration/http/cluster_specifier/golang.rst +++ b/docs/root/configuration/http/cluster_specifier/golang.rst @@ -11,6 +11,6 @@ Go cluster specifier plugins can be recompiled independently of Envoy. .. warning:: The Envoy Golang cluster specifier is designed to be run with the ``GODEBUG=cgocheck=0`` environment variable set. - This disables the cgo pointer check. + This disables the `cgo `_ pointer check. Failure to set this environment variable will cause Envoy to crash! diff --git a/docs/root/configuration/http/http_conn_man/header_casing.rst b/docs/root/configuration/http/http_conn_man/header_casing.rst index ea94c0a2dfd34..b6098bf20537a 100644 --- a/docs/root/configuration/http/http_conn_man/header_casing.rst +++ b/docs/root/configuration/http/http_conn_man/header_casing.rst @@ -11,10 +11,10 @@ To support these use cases, Envoy allows :ref:`configuring a formatting scheme f `, which will have Envoy transform the header keys during serialization. -To configure this formatting on response headers, specify the format in the +To configure this formatting on upstream request headers, specify the format in the :ref:`http_protocol_options `. -To configure this for upstream request headers, specify the formatting in +To configure this for downstream response headers, specify the formatting in :ref:`http_protocol_options ` in the cluster's :ref:`extension_protocol_options`. diff --git a/docs/root/configuration/http/http_conn_man/stats.rst b/docs/root/configuration/http/http_conn_man/stats.rst index e9af79c1cc844..e81b883dde337 100644 --- a/docs/root/configuration/http/http_conn_man/stats.rst +++ b/docs/root/configuration/http/http_conn_man/stats.rst @@ -163,10 +163,15 @@ On the upstream side all http2 statistics are rooted at ``cluster..http2.` :header: Name, Type, Description :widths: 1, 1, 2 + ``cookie_count``, Histogram, Number of individial ``cookie`` headers. Enabled by setting the ``envoy.reloadable_features.http2_record_histograms`` to ``true``. + ``cookie_size``, Histogram, Byte size of the re-assebled ``cookie`` header. Enabled by setting the ``envoy.reloadable_features.http2_record_histograms`` to ``true``. + ``cookies_total_bytes_too_large``, Counter, Total number of streams reset due to the re-assembled ``cookie`` header exceeding the ``envoy.reloadable_features.http2_max_cookies_size_in_kb`` runtime value. ``dropped_headers_with_underscores``, Counter, Total number of dropped headers with names containing underscores. This action is configured by setting the :ref:`headers_with_underscores_action config setting `. ``goaway_sent``, Counter, Total number ``GOAWAY`` frames that have been submitted to the codec to send. ``header_overflow``, Counter, Total number of connections reset due to the headers being larger than the :ref:`configured value `. ``headers_cb_no_stream``, Counter, Total number of errors where a header callback is called without an associated stream. This tracks an unexpected occurrence due to an as yet undiagnosed bug + ``header_count``, Histogram, Number of headers including individual ``cookie`` headers. Enabled by setting the ``envoy.reloadable_features.http2_record_histograms`` to ``true``. + ``header_list_size``, Histogram, Byte size of all headers including ``cookie`` headers. Enabled by setting the ``envoy.reloadable_features.http2_record_histograms`` to ``true``. ``inbound_empty_frames_flood``, Counter, Total number of connections terminated for exceeding the limit on consecutive inbound frames with an empty payload and no end stream flag. The limit is configured by setting the :ref:`max_consecutive_inbound_frames_with_empty_payload config setting `. ``inbound_priority_frames_flood``, Counter, Total number of connections terminated for exceeding the limit on inbound frames of type PRIORITY. The limit is configured by setting the :ref:`max_inbound_priority_frames_per_stream config setting `. ``inbound_window_update_frames_flood``, Counter, Total number of connections terminated for exceeding the limit on inbound frames of type WINDOW_UPDATE. The limit is configured by setting the :ref:`max_inbound_window_updateframes_per_data_frame_sent config setting `. diff --git a/docs/root/configuration/http/http_filters/_include/bandwidth-share-filter.yaml b/docs/root/configuration/http/http_filters/_include/bandwidth-share-filter.yaml new file mode 100644 index 0000000000000..22f4d81da1430 --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/bandwidth-share-filter.yaml @@ -0,0 +1,77 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + 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 + codec_type: AUTO + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: {prefix: "/path/with/bandwidth/share"} + route: {cluster: service_protected_by_bandwidth_share} + typed_per_filter_config: + envoy.filters.http.bandwidth_share: + "@type": type.googleapis.com/envoy.extensions.filters.http.bandwidth_share.v3.BandwidthShare + request_limit: + bucket_id: foo + kbps: + default_value: 10000 + runtime_key: bandwidth_share_limit_foo + fill_interval: 0.1s + tenant_name_selector: + on_no_match: + action: + name: use_client_certificate_name_as_tenant_name + typed_config: + "@type": type.googleapis.com/envoy.config.core.v3.SubstitutionFormatString + omit_empty_values: true + text_format_source: + inline_string: '%CEL(re.extract(connection.subject_peer_certificate, R"CN=([^,]*)", "\\1"))%' + formatters: + - name: envoy.formatter.cel + typed_config: + "@type": type.googleapis.com/envoy.extensions.formatter.cel.v3.Cel + - match: {prefix: "/"} + route: {cluster: web_service} + http_filters: + - name: envoy.filters.http.bandwidth_share + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.bandwidth_share.v3.BandwidthShare + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: service_protected_by_bandwidth_share + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: web_service + port_value: 9000 + - name: web_service + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: service1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: web_service + port_value: 9000 diff --git a/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml b/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml new file mode 100644 index 0000000000000..d7b4193873f1c --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml @@ -0,0 +1,111 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + 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 + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + - address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + 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: ingress1_http + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_datasource: + filename: "/geoip/ip-tags.yaml" + watched_directory: + path: "/geoip/" + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + - address: + socket_address: + address: 0.0.0.0 + port_value: 7000 + 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: ingress2_http + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_datasource: + filename: "/geoip/ip-tags.json" + watched_directory: + path: "/geoip/" + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + clusters: + - name: default_service + load_assignment: + cluster_name: default_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 +admin: + address: + socket_address: + address: 0.0.0.0 + port_value: 9901 diff --git a/docs/root/configuration/http/http_filters/_include/jwt-authn-extract-only-rbac.yaml b/docs/root/configuration/http/http_filters/_include/jwt-authn-extract-only-rbac.yaml new file mode 100644 index 0000000000000..8108544de3bd8 --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/jwt-authn-extract-only-rbac.yaml @@ -0,0 +1,91 @@ +# Example Envoy bootstrap config showing how to use +# extract_only_without_validation with an RBAC policy that checks +# the verification status header before trusting JWT-derived headers. +# +# The RBAC policy only allows requests where: +# 1. The JWT claim 'role' equals 'admin', AND +# 2. The x-jwt-signature-verified header is NOT 'false' +# This prevents forged JWTs from satisfying the admin role check. +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + protocol: TCP + address: 0.0.0.0 + port_value: 10000 + 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 + route_config: + name: local_route + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/admin" + direct_response: + status: 200 + body: + inline_string: "admin OK" + http_filters: + - name: envoy.filters.http.jwt_authn + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + example_provider: + issuer: https://example.com + claim_to_headers: + - header_name: x-jwt-claim-role + claim_name: role + # extract_only_without_validation does not verify signatures, + # but the JWKS must still parse and contain at least one valid + # public key. This dummy RSA key satisfies that requirement + # without being used to verify anything. + local_jwks: + inline_string: >- + {"keys": [ + {"kty":"RSA", + "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z + wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc + 5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8K + JZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh + 6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKn + qDKgw", + "e":"AQAB", + "alg":"RS256", + "kid":"dummy"}]} + rules: + - match: + prefix: "/admin" + requires: + extract_only_without_validation: {} + - name: envoy.filters.http.rbac + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: ALLOW + policies: + admin_verified_only: + permissions: + - any: true + principals: + - and_ids: + ids: + - header: + name: x-jwt-claim-role + string_match: + exact: admin + - not_id: + header: + name: x-jwt-signature-verified + string_match: + exact: "false" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router diff --git a/docs/root/configuration/http/http_filters/aws_lambda_filter.rst b/docs/root/configuration/http/http_filters/aws_lambda_filter.rst index 18290f403ec6e..d3e020aec8507 100644 --- a/docs/root/configuration/http/http_filters/aws_lambda_filter.rst +++ b/docs/root/configuration/http/http_filters/aws_lambda_filter.rst @@ -101,7 +101,7 @@ Below are some examples that show how the filter can be used in different deploy Example configuration --------------------- -In this configuration, the filter applies to all routes in the filter chain of the http connection manager: +In this configuration, the filter applies to all routes in the filter chain of the HTTP connection manager: .. literalinclude:: _include/aws-lambda-filter.yaml :language: yaml diff --git a/docs/root/configuration/http/http_filters/bandwidth_share_filter.rst b/docs/root/configuration/http/http_filters/bandwidth_share_filter.rst new file mode 100644 index 0000000000000..5c5772413a5d0 --- /dev/null +++ b/docs/root/configuration/http/http_filters/bandwidth_share_filter.rst @@ -0,0 +1,68 @@ +.. _config_http_filters_bandwidth_share: + +Bandwidth share +=============== + +* Bandwidth limiting :ref:`architecture overview ` +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.bandwidth_share.v3.BandwidthShare``. +* :ref:`v3 API reference ` + +The HTTP Bandwidth share filter limits the size of data flow to the max bandwidth set in ``request_limit.kbps`` +or ``response_limit.kbps`` when the request's route, virtual host or filter chain has a +:ref:`bandwidth share configuration `. + +If the bandwidth limit has been exhausted the filter stops further transfer until more bandwidth gets allocated +according to the ``fill_interval`` (default is 50 milliseconds). If the connection buffer fills up with accumulated +data then the source of data will have ``readDisable(true)`` set as described in the :repo:`flow control doc`. + +When actively being limited, the filter splits the available bandwidth between active tenants by weight, and +between parallel requests for a single tenant evenly. For example, with six active requests divided +among three tenants: + +====== ====== =============== ================ +Tenant Weight Share to tenant Share to request +====== ====== =============== ================ +foo 1 20% 6.6̅% +bar 3 60% 30% +foo 1 20% 6.6̅% +foo 1 20% 6.6̅% +bar 3 60% 30% +baz 1 20% 20% +====== ====== =============== ================ + +.. note:: + The token bucket is shared across all workers, thus the limits are applied per Envoy process. + +Example configuration +--------------------- + +Example filter configuration for a globally enabled bandwidth share but disabled for a specific route: + +.. literalinclude:: _include/bandwidth-share-filter.yaml + :language: yaml + :lines: 11-53 + :emphasize-lines: 9-25 + :caption: :download:`bandwidth-share-filter.yaml <_include/bandwidth-share-filter.yaml>` + +Statistics +---------- + +The HTTP bandwidth share filter outputs statistics in the ``.bandwidth_share.`` namespace. + +All metrics are emitted with the following tags: + +* ``bucket_id`` (may be empty-string, as configured in ``request_limit`` or ``response_limit``) +* ``tenant`` (if not explicitly configured for inclusion with ``include_stats_tag`` in ``tenant_config``, will be empty-string) +* ``direction`` (``request`` or ``response``) + +The counter ``bytes`` is additionally emitted with the following tag: + +* ``handling`` (``limited`` or ``not_limited`` - ``not_limited`` is used for bytes for which the limiter remained in "fast" mode as the configured limit had not been approached.) + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + bytes, Counter, Number of bytes for which the bandwidth share filter was consulted. + streams_currently_limited, Gauge, Number of streams that are currently experiencing delays. + bytes_pending, Gauge, Number of bytes that are currently buffered due to delays. diff --git a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst index 34090f4d0069e..024b88088a4e1 100644 --- a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst +++ b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst @@ -147,6 +147,7 @@ namespace. host_removed, Counter, Number of hosts that have been removed from the cache. num_hosts, Gauge, Number of hosts that are currently in the cache. dns_rq_pending_overflow, Counter, Number of DNS pending request overflow. + dns_address_filter_out, Counter, Number of resolved addresses that were denied by :ref:`resolved_address_filter `. The dynamic forward proxy DNS cache circuit breakers output statistics in the ``dns_cache..circuit_breakers`` namespace. diff --git a/docs/root/configuration/http/http_filters/filter_chain_filter.rst b/docs/root/configuration/http/http_filters/filter_chain_filter.rst new file mode 100644 index 0000000000000..3e7cbaab5c32e --- /dev/null +++ b/docs/root/configuration/http/http_filters/filter_chain_filter.rst @@ -0,0 +1,206 @@ +.. _config_http_filters_filter_chain: + +Filter Chain +============ + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfig``. +* :ref:`v3 API reference ` + +The filter chain filter acts as a wrapper that applies a configurable set of HTTP filters to +incoming requests. It supports a ``default_filter_chain`` at the filter level and optional +per-route overrides, all merged together using name-based override semantics. + +Overview +-------- + +When a request arrives the filter collects all active filter chains in order from least to +most specific: + +1. ``default_filter_chain`` (from the filter-level ``FilterChainConfig``) +2. Per-route chains from outermost to innermost scope (e.g. virtual-host level, then route level) + +Each filter in a less-specific chain is applied **unless** a more-specific chain contains a +filter with the same ``name``. In that case the less-specific entry is silently skipped. + +This lets per-route configuration selectively extend or replace the default chain without +redefining it entirely. + +If no chains are resolved for a request the filter passes through without any modification and +the ``pass_through`` counter is incremented. + +Configuration +------------- + +Filter-level Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The filter-level configuration (:ref:`FilterChainConfig `) +accepts one optional field: + +* ``default_filter_chain``: The default chain applied to every request. Can be overridden per + route using ``FilterChainConfigPerRoute``. + +Per-Route Configuration +~~~~~~~~~~~~~~~~~~~~~~~ + +The per-route configuration (:ref:`FilterChainConfigPerRoute `) +accepts one required field: + +* ``filter_chain``: An inline filter chain. Filters in this chain override same-named filters + from less-specific chains (e.g. the default chain). + +Statistics +---------- + +The filter emits the following counters under the ``filter_chain.`` namespace: + +.. csv-table:: + :header: Name, Type, Description + :widths: auto + + pass_through, Counter, Number of requests for which no filter chain was resolved (the filter passed through without modification) + +Example Configurations +---------------------- + +Basic Default Chain +~~~~~~~~~~~~~~~~~~~ + +Applies a header-mutation filter to every request by default: + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.filter_chain + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfig + default_filter_chain: + filters: + - name: envoy.filters.http.buffer + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: x-default-tag + value: "true" + append_action: APPEND_IF_EXISTS_OR_ADD + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + +Per-Route Filter Chain +~~~~~~~~~~~~~~~~~~~~~~ + +The default chain adds ``x-default-tag`` to every request. The ``/upload/`` route adds its +own ``x-upload-tag`` header. Both filters run because their names are distinct — the +per-route chain extends the default rather than replacing it: + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.filter_chain + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfig + default_filter_chain: + filters: + - name: envoy.filters.http.header_mutation + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: x-default-tag + value: "true" + append_action: APPEND_IF_EXISTS_OR_ADD + + routes: + - match: + prefix: /upload/ + route: + cluster: upload_cluster + typed_per_filter_config: + envoy.filters.http.filter_chain: + "@type": type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfigPerRoute + filter_chain: + filters: + - name: add-upload-tag # distinct name — does NOT override the default + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: x-upload-tag + value: "true" + append_action: APPEND_IF_EXISTS_OR_ADD + +Overriding a Default Filter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default chain and the per-route chain both configure a filter named +``envoy.filters.http.header_mutation``. Because the names match, the per-route definition +wins — only the per-route version of that filter runs on the ``/api/`` route. The default +version is skipped entirely: + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.filter_chain + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfig + default_filter_chain: + filters: + - name: envoy.filters.http.header_mutation + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: x-tag + value: default + append_action: APPEND_IF_EXISTS_OR_ADD + + routes: + - match: + prefix: /api/ + route: + cluster: api_cluster + typed_per_filter_config: + envoy.filters.http.filter_chain: + "@type": type.googleapis.com/envoy.extensions.filters.http.filter_chain.v3.FilterChainConfigPerRoute + filter_chain: + filters: + - name: envoy.filters.http.header_mutation # same name — overrides the default + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: x-tag + value: api-specific + append_action: APPEND_IF_EXISTS_OR_ADD + +Behavior Notes +-------------- + +* **No chains resolved**: If no filter chain exists at either the filter level or the route + level the filter passes through without modification and increments ``pass_through``. +* **Merge order**: The default chain always runs first (least specific). Per-route chains run + after in scope order from outermost (virtual-host) to innermost (route). Within each chain + filters are applied in the order they are listed. +* **Override by name**: A filter in a less-specific chain is skipped if any more-specific chain + defines a filter with the same ``name`` field. The ``name`` field is the sole key for + override resolution — the typed config type does not matter. +* **Route match timing**: Only the initial route match determines which per-route chains are + collected. Subsequent internal route refreshes do not change the active chains. +* **Override change order**: If a filter X is overridden by name in a more-specific chain, + the less-specific X is skipped entirely and the most-specific one will run. But note + that the order of the filters in the final chain is always from least to most specific, so the + less-specific filters run first and the X from the more-specific chain runs + after previous filters. This changes the order of execution for X. If the order matters, + consider to override the whole chain. diff --git a/docs/root/configuration/http/http_filters/geoip_filter.rst b/docs/root/configuration/http/http_filters/geoip_filter.rst index 827eb0b01ea7c..b743e19a57676 100644 --- a/docs/root/configuration/http/http_filters/geoip_filter.rst +++ b/docs/root/configuration/http/http_filters/geoip_filter.rst @@ -61,7 +61,7 @@ per geolocation database type (rooted at ``.maxmind.``). Database t ``.total``, Counter, Total number of lookups performed for a given geolocation database file. ``.hit``, Counter, Total number of successful lookups (with non empty lookup result) performed for a given geolocation database file. - ``.lookup_error``, Counter, Total number of errors that occured during lookups for a given geolocation database file. + ``.lookup_error``, Counter, Total number of errors that occurred during lookups for a given geolocation database file. ``.db_reload_success``, Counter, Total number of times when the geolocation database file was reloaded successfully. ``.db_reload_error``, Counter, Total number of times when the geolocation database file failed to reload. ``.db_build_epoch``, Gauge, The build timestamp of the geolocation database file represented as a Unix epoch value. diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index 61fafd48fe643..ad47b4a0b509d 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -13,6 +13,7 @@ HTTP filters api_key_auth_filter aws_request_signing_filter bandwidth_limit_filter + bandwidth_share_filter basic_auth_filter buffer_filter cache_filter @@ -34,6 +35,7 @@ HTTP filters fault_filter file_server_filter file_system_buffer_filter + filter_chain_filter gcp_authn_filter geoip_filter golang_filter @@ -73,5 +75,6 @@ HTTP filters tap_filter thrift_to_metadata_filter upstream_codec_filter + upstream_rbac_filter wasm_filter transform_filter diff --git a/docs/root/configuration/http/http_filters/ip_tagging_filter.rst b/docs/root/configuration/http/http_filters/ip_tagging_filter.rst index 73e4a87c05dfd..941d8aee2f867 100644 --- a/docs/root/configuration/http/http_filters/ip_tagging_filter.rst +++ b/docs/root/configuration/http/http_filters/ip_tagging_filter.rst @@ -22,12 +22,82 @@ described in the paper `IP-address lookup using LC-tries `_ by S. Nilsson and G. Karlsson. +IP tags can either be provided directly using the :ref:`ip_tags ` API field or +can be loaded from file if :ref:`ip_tags_datasource ` API field is configured. +For file based IP tags YAML and JSON file formats are supported. +IP tags will be dynamically reloaded if ``watched_directory`` is configured for :ref:`ip_tags_datasource `. Configuration ------------- * This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging``. * :ref:`v3 API reference ` +An example configuration of the filter with inline ip tags may look like the following: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 13-21 + :lineno-start: 13 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Below is an example configuration of the filter with the file based ip tags in yaml format: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 44-54 + :lineno-start: 44 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Where the *ip-tags.yaml* file would have the following content: + +.. code-block:: yaml + + ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + +And here is an example configuration of the filter with the file based IP tags in JSON format: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 77-87 + :lineno-start: 77 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Where the ``ip-tags.json`` file would have the following content: + +.. code-block:: json + + { + "ip_tags": [ + { + "ip_tag_name": "external_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + }, + { + "ip_tag_name": "internal_request", + "ip_list": [ + { + "address_prefix": "1.2.3.5", + "prefix_len": 32 + } + ] + } + ] + } + Statistics ---------- @@ -42,6 +112,14 @@ the owning HTTP connection manager. no_hit, Counter, Total number of requests with no applicable IP tags total, Counter, Total number of requests the IP Tagging Filter operated on +When file based reload of IP tags is enabled, additional reload stats will be available in the ``http..ip_tagging_reload.`` namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + success, Counter, Total number of successful reloads of IP tags file + Runtime ------- diff --git a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst index 6d896e1c50151..5ddc90e47b636 100644 --- a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst +++ b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst @@ -235,3 +235,55 @@ comes from the owning HTTP connection manager. jwks_fetch_failed, Counter, Total failed JWKS remote fetch attempts jwt_cache_hit, Counter, Total JWT cache hits where a previously validated token was reused jwt_cache_miss, Counter, Total JWT cache misses requiring full token validation + + +.. _config_jwt_authn_extract_only_security: + +Extract-Only Mode Security Considerations +------------------------------------------ + +.. warning:: + + **SECURITY WARNING**: The ``extract_only_without_validation`` + requirement type does **NOT** verify JWT signatures. Any party can craft + a JWT with arbitrary claims, and those claims will be extracted and + forwarded as HTTP headers. + + **Verification status header (default on):** + + When this mode is active, Envoy sets a verification status header on + requests whose JWT is present but fails signature verification: + + .. code-block:: yaml + + x-jwt-signature-verified: false + + The header is NOT set when the JWT is valid or when no JWT is present. + This means the header's presence is a meaningful signal: if set, the + JWT failed verification and downstream filters should not trust any + JWT-derived claim headers. + + **RBAC integration:** + + If RBAC policies match on JWT-derived claim headers (e.g., + ``x-jwt-claim-role``), add a corresponding principal check for the + verification status header. Example RBAC policy that only allows + verified admin claims: + + .. literalinclude:: _include/jwt-authn-extract-only-rbac.yaml + :language: yaml + :lines: 58-73 + :lineno-start: 58 + :linenos: + :caption: :download:`jwt-authn-extract-only-rbac.yaml <_include/jwt-authn-extract-only-rbac.yaml>` + + **Configuration:** + + The header name is configurable via ``verification_status_header`` in + ``ExtractOnlyWithoutValidation``. The header is set by default, but this behavior can be + disabled via the ``envoy.reloadable_features.jwt_authn_add_verification_status_header`` + runtime flag. + + **Recommended alternative:** Use ``provider_name`` with ``remote_jwks`` + or ``local_jwks`` for full signature verification. + diff --git a/docs/root/configuration/http/http_filters/lua_filter.rst b/docs/root/configuration/http/http_filters/lua_filter.rst index 261896b6442ec..4788908540699 100644 --- a/docs/root/configuration/http/http_filters/lua_filter.rst +++ b/docs/root/configuration/http/http_filters/lua_filter.rst @@ -140,6 +140,11 @@ individual filter instance/script can be tracked by providing a per-filter errors, Counter, Total script execution errors. executions, Counter, Total number of times ``envoy_on_request`` and ``envoy_on_response`` was executed. +In addition, a single process-wide ``lua.lua_vm_count`` gauge (not affected by ``stat_prefix``) tracks +the total number of active Lua VMs across every filter-config-level and route-level Lua script +configured in the process. Each configured script accounts for ``concurrency + 1`` VMs (one per +worker thread, plus the main thread). + Script examples --------------- @@ -719,6 +724,17 @@ an empty metadata object. Returns a :ref:`route object `. +``stats()`` +^^^^^^^^^^^ + +.. code-block:: lua + + local stats = handle:stats() + +Returns a stats scope object that can be used to create and modify stats (counters, gauges, and +histograms). See :ref:`Stats scope object API ` for +available methods. + .. _config_http_filters_lua_header_wrapper: Header object API @@ -1358,7 +1374,7 @@ Sets a filter state object by name using a registered :ref:`object factory ` for the list of available factory keys. * ``payload`` is a string passed to the factory's ``createFromBytes`` method. -The object is stored as read-only with filter chain lifespan and no upstream sharing. +The object is stored with filter chain lifespan and no upstream sharing. Raises a Lua error if the factory key is not registered or if the factory fails to create an object from the given payload. @@ -1796,3 +1812,156 @@ Below is an example of a ``metadata`` in a :ref:`route entry ` Returns a :ref:`metadata object `. + +.. _config_http_filters_lua_stats_scope_wrapper: + +Stats scope object API +---------------------- + +.. code-block:: lua + + local stats = handle:stats() + +``handle:stats()`` returns a stats scope object that can be used to create and +modify stats (counters, gauges, and histograms). Stats created through this API +are prefixed with ``lua.`` followed by the optional ``stat_prefix`` from the Lua +filter configuration. + +counter() +^^^^^^^^^ + +.. code-block:: lua + + local counter = stats:counter("my_counter") + +Returns a :ref:`counter object ` with +the given name. The counter is created if it doesn't exist. + +gauge() +^^^^^^^ + +.. code-block:: lua + + local gauge = stats:gauge("my_gauge") + +Returns a :ref:`gauge object ` with +the given name. The gauge is created if it doesn't exist. Gauges created through +Lua use ``NeverImport`` mode, meaning they track local process state and are not +aggregated across the cluster during hot restart. + +histogram() +^^^^^^^^^^^ + +.. code-block:: lua + + local histogram = stats:histogram("my_histogram", "ms") + +Returns a :ref:`histogram object ` +with the given name and unit. The second argument specifies the unit and must be +one of: ``"unspecified"``, ``"bytes"``, ``"microseconds"``, ``"milliseconds"``, or ``"ms"`` +(shorthand for milliseconds). + +.. _config_http_filters_lua_counter_wrapper: + +Counter object API +------------------ + +inc() +^^^^^ + +.. code-block:: lua + + counter:inc() + +Increments the counter by 1. + +add() +^^^^^ + +.. code-block:: lua + + counter:add(5) + +Adds the specified value to the counter. The value must be a non-negative integer. + +value() +^^^^^^^ + +.. code-block:: lua + + local val = counter:value() + +Returns the current value of the counter. + +.. _config_http_filters_lua_gauge_wrapper: + +Gauge object API +---------------- + +inc() +^^^^^ + +.. code-block:: lua + + gauge:inc() + +Increments the gauge by 1. + +dec() +^^^^^ + +.. code-block:: lua + + gauge:dec() + +Decrements the gauge by 1. + +add() +^^^^^ + +.. code-block:: lua + + gauge:add(5) + +Adds the specified value to the gauge. The value must be a non-negative integer. + +sub() +^^^^^ + +.. code-block:: lua + + gauge:sub(3) + +Subtracts the specified value from the gauge. The value must be a non-negative integer. + +set() +^^^^^ + +.. code-block:: lua + + gauge:set(10) + +Sets the gauge to the specified value. The value must be a non-negative integer (zero is valid). + +value() +^^^^^^^ + +.. code-block:: lua + + local val = gauge:value() + +Returns the current value of the gauge. + +.. _config_http_filters_lua_histogram_wrapper: + +Histogram object API +-------------------- + +recordValue() +^^^^^^^^^^^^^ + +.. code-block:: lua + + histogram:recordValue(42) + +Records a value in the histogram. The value must be a non-negative integer. diff --git a/docs/root/configuration/http/http_filters/mcp_router_filter.rst b/docs/root/configuration/http/http_filters/mcp_router_filter.rst index 9fe7648b4a5cb..1711714e12dbb 100644 --- a/docs/root/configuration/http/http_filters/mcp_router_filter.rst +++ b/docs/root/configuration/http/http_filters/mcp_router_filter.rst @@ -23,12 +23,50 @@ Example configuration: - name: envoy.filters.http.mcp_router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.mcp_router.v3.McpRouter + lazy_initialization: true servers: - name: backend1 mcp_cluster: cluster: backend1_cluster path: /mcp +.. _config_http_filters_mcp_router_lazy_initialization: + +Lazy initialization +~~~~~~~~~~~~~~~~~~~ + +By default, the MCP router eagerly initializes all backend servers during the client's ``initialize`` +request, blocking until every backend responds (or times out). When ``lazy_initialization`` is set to +``true``, the ``initialize`` response is returned immediately with gateway capabilities and an empty +backend session map. Each backend is then initialized on-demand when a request first routes to it. + +This is useful when some backends are slow or unreliable and should not block client initialization. + +.. _config_http_filters_mcp_router_server_requests: + +Server-to-client requests +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The MCP protocol allows backend servers to send requests to clients mid-stream. This includes +``elicitation/create`` (requesting additional input from the user), ``sampling/createMessage`` +(requesting LLM completions), and ``roots/list`` (querying available roots). + +The MCP router handles these transparently: + +1. When a backend sends a server-to-client request via SSE, the gateway forwards it to the client. + In multiplexing mode (multiple backends), the JSON-RPC ``id`` field is rewritten to include the + backend name as a prefix (e.g., ``42`` becomes ``"time__42"``), enabling correct routing of the + client's response. + +2. When the client sends a JSON-RPC response back, the gateway parses the prefixed ``id`` to + determine which backend should receive the response, restores the original ``id`` value, and + forwards the response to that backend. + +In single-backend mode, no ``id`` rewriting is performed since there is only one possible target. + +No configuration is required. The gateway advertises ``elicitation`` capability to clients +automatically and handles the request/response routing based on the client's declared capabilities. + .. _config_http_filters_mcp_router_statistics: Statistics diff --git a/docs/root/configuration/http/http_filters/rate_limit_filter.rst b/docs/root/configuration/http/http_filters/rate_limit_filter.rst index 69823cdbab17b..0e349ce0d36d0 100644 --- a/docs/root/configuration/http/http_filters/rate_limit_filter.rst +++ b/docs/root/configuration/http/http_filters/rate_limit_filter.rst @@ -173,6 +173,10 @@ The ratelimit filter emits dynamic metadata as an opaque ``google.protobuf.Struc ` with a filled :ref:`dynamic_metadata ` field. +By default metadata is stored under the ``envoy.filters.http.ratelimit`` namespace. The namespace can be changed by setting the +:ref:`metadata_namespace ` in the filter +configuration. + Runtime ------- diff --git a/docs/root/configuration/http/http_filters/tap_filter.rst b/docs/root/configuration/http/http_filters/tap_filter.rst index b63b7b41099d1..4f4d3677056bc 100644 --- a/docs/root/configuration/http/http_filters/tap_filter.rst +++ b/docs/root/configuration/http/http_filters/tap_filter.rst @@ -169,9 +169,9 @@ conditions are met, the request will be tapped and streamed out to the admin end .. attention:: - Searching for patterns in HTTP body is potentially cpu intensive. For each specified pattern, http body is scanned byte by byte to find a match. + Searching for patterns in HTTP body is potentially cpu intensive. For each specified pattern, HTTP body is scanned byte by byte to find a match. If multiple patterns are specified, the process is repeated for each pattern. If location of a pattern is known, ``bytes_limit`` should be specified - to scan only part of the http body. + to scan only part of the HTTP body. Output format ------------- @@ -304,6 +304,36 @@ might look like this: Etc. +Sampling +-------- + +Configure :ref:`tap_enabled +` to sample a +fraction of requests at the tap filter: + +.. code-block:: yaml + + tap_enabled: + default_value: + numerator: 1 + denominator: TEN_THOUSAND + runtime_key: tap.sampling.my_route + +When sampling is configured, only the configured fraction of requests +proceeds to match-predicate evaluation. The remainder is short-circuited +before any match state is allocated. Sampled-out requests increment the +``rq_sampled_out`` filter stat. Sampling applies to whichever tap +configuration is active, whether installed statically or through the +``/tap`` admin endpoint. + +The configured sampling rate is recorded on +:ref:`configured_sample_rate +` of the +first emitted ``TraceWrapper`` segment of each tap trace. When a +runtime override of ``runtime_key`` is active, the effective rate may +differ from the recorded value. The :ref:`tap transport socket +` supports the same per-connection sampling. + Statistics ---------- @@ -315,4 +345,5 @@ comes from the owning HTTP connection manager. :header: Name, Type, Description :widths: 1, 1, 2 + rq_sampled_out, Counter, Total requests short-circuited by ``tap_enabled`` sampling before match evaluation rq_tapped, Counter, Total requests that matched and were tapped diff --git a/docs/root/configuration/http/http_filters/upstream_rbac_filter.rst b/docs/root/configuration/http/http_filters/upstream_rbac_filter.rst new file mode 100644 index 0000000000000..f1872b00b0e92 --- /dev/null +++ b/docs/root/configuration/http/http_filters/upstream_rbac_filter.rst @@ -0,0 +1,145 @@ +.. _config_http_filters_upstream_rbac: +.. _extension_envoy.filters.http.upstream_rbac: + +Upstream Role Based Access Control (RBAC) Filter +================================================ + +The upstream RBAC filter authorizes a request against the **selected upstream host**, before the +upstream connection is initiated. It is configured exactly like the +:ref:`RBAC filter ` (it reuses the same +:ref:`RBAC ` configuration), but runs as an +:ref:`upstream HTTP filter ` and evaluates its policies from the +host-selection callback rather than during request decoding. + +The primary use case is protecting against SSRF by enforcing a default-deny policy on the upstream +IP address. Because evaluation happens after a host has been selected but before the connection is +established, the :ref:`upstream_ip_port +` matcher +receives the resolved upstream address directly from host selection. By contrast, the downstream +:ref:`RBAC filter ` can only match on the upstream IP when a preceding HTTP +filter (the dynamic forward proxy filter with ``save_upstream_address``) has already recorded the +selected host in filter state. Evaluating at host-selection time removes that dependency, so the +upstream IP match works for **all** cluster types — including dynamic forward proxy +``sub_cluster_config``, static, EDS, and strict DNS. When the request is denied, no upstream +connection is established and a ``403 (Forbidden)`` local reply is returned. + +Since the filter is handed the downstream connection and request headers, the full set of RBAC +matchers available to the downstream :ref:`RBAC filter ` (request headers, +source/destination IP and port, SSL properties, dynamic metadata, CEL attributes, ...) can be +combined with the upstream IP match. + +.. attention:: + + This filter must be configured as an **upstream** HTTP filter. There is no top-level + ``upstream_http_filters`` field on a cluster; the upstream HTTP filter chain lives in one of two + places: + + * **Per cluster** — inside the cluster's + :ref:`typed_extension_protocol_options `, + under the ``envoy.extensions.upstreams.http.v3.HttpProtocolOptions`` entry, in its + :ref:`http_filters ` + list (see the example below). + * **On the router** (applies to every cluster) — via + :ref:`Router.upstream_http_filters `. + + Either chain must end with the ``envoy.filters.http.upstream_codec`` terminal filter. The filter + has no effect in a downstream HTTP filter chain, and is therefore registered only as an upstream + filter. + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC``. +* :ref:`v3 API reference ` + +Example configuration +--------------------- + +The following ``Cluster`` denies any request whose selected upstream IP falls within a private CIDR +range. The upstream filter chain is configured **on the cluster**, under +``typed_extension_protocol_options`` → ``envoy.extensions.upstreams.http.v3.HttpProtocolOptions`` → +``http_filters`` (the same ``HttpProtocolOptions`` entry that carries the upstream protocol config), +and ends with the ``upstream_codec`` terminal filter: + +.. code-block:: yaml + + clusters: + - name: egress_cluster + connect_timeout: 5s + type: STRICT_DNS + load_assignment: {} # ... cluster endpoints ... + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + # Upstream protocol selection (required by HttpProtocolOptions). + explicit_http_config: + http_protocol_options: {} + # Upstream HTTP filter chain. Must end with envoy.filters.http.upstream_codec. + http_filters: + - name: envoy.filters.http.upstream_rbac + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC + rules: + action: DENY + policies: + "deny-private-ips": + permissions: + - matcher: + name: envoy.rbac.matchers.upstream_ip_port + typed_config: + "@type": type.googleapis.com/envoy.extensions.rbac.matchers.upstream_ip_port.v3.UpstreamIpPortMatcher + upstream_ip: + address_prefix: 10.0.0.0 + prefix_len: 8 + principals: + - any: true + - name: envoy.filters.http.upstream_codec + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.upstream_codec.v3.UpstreamCodec + +Statistics +---------- + +The upstream RBAC filter outputs the same statistics as the +:ref:`RBAC filter `, with the counter names built as +``.rbac.[.]{allowed,denied,shadow_allowed,shadow_denied}``. The +```` prefix depends on **where the filter is attached**, not on the filter itself: + +* When attached to the router via :ref:`Router.upstream_http_filters + `, the stats + inherit the router's (HTTP connection manager / listener) scope. On a listener with no stats + prefix this is the **root** scope, so the counters are simply ``rbac.*`` (e.g. + ``rbac..allowed``). This produces a single aggregate per Envoy, independent of + which upstream cluster is selected at request time. +* When attached to a cluster via + :ref:`HttpProtocolOptions.http_filters + `, the stats are + emitted in that **cluster's** scope, i.e. ``cluster..rbac.*``. + +.. note:: + + With :ref:`dynamic forward proxy ` + ``sub_cluster_config``, the cluster selected at request time is a per-host dynamic sub-cluster + (``DFPCluster::``). Attaching the filter at the **cluster** scope in that mode would + therefore emit one counter series per resolved upstream host — i.e. unbounded cardinality — and + scatter deny counts across many series. Prefer attaching the filter at the **router** scope for + the dynamic forward proxy use case so the counters stay a single root-scoped aggregate. + +The optional ``rules_stat_prefix`` / ``shadow_rules_stat_prefix`` fields of the +:ref:`RBAC ` config add a further segment +after ``rbac.`` (e.g. ``rules_stat_prefix: private`` yields ``rbac.private.allowed``); this is +orthogonal to the scope prefix above. + +Dynamic Metadata +---------------- + +The upstream RBAC filter emits the same dynamic metadata as the +:ref:`RBAC filter `, under the +``envoy.filters.http.upstream_rbac`` key namespace. + +Per-Route Configuration +----------------------- + +Like the downstream :ref:`RBAC filter `, the engine can be overridden or +disabled per virtual host / route / weighted cluster by attaching a +:ref:`RBACPerRoute ` to the route's +``typed_per_filter_config``, keyed by this filter's configured name. The override is resolved over +the request's (downstream) route, so the same route entry can carry distinct downstream and upstream +RBAC overrides. diff --git a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst index 761f8dfaf5145..a9623a02d31b0 100644 --- a/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst +++ b/docs/root/configuration/listeners/listener_filters/proxy_protocol.rst @@ -66,6 +66,29 @@ The filter supports two storage locations for TLV values, controlled by the action: name: allow +TLV Value Encoding +------------------ + +By default, TLV values are sanitized to valid UTF-8 strings before being stored in +dynamic metadata or filter state: any invalid UTF-8 sequences are replaced with the +``!`` character. For binary TLV values, the +:ref:`value_string_encoding ` +option can be set to ``BASE64`` to store the raw TLV value as a base64-encoded string instead. +Note that this option only applies to the legacy untyped dynamic metadata and filter state; +the typed dynamic metadata always stores the raw TLV value bytes as is: + +.. code-block:: yaml + + listener_filters: + - name: envoy.filters.listener.proxy_protocol + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol + rules: + - tlv_type: 0xEA + on_tlv_present: + key: "aws_vpce_id" + value_string_encoding: BASE64 + This implementation supports both version 1 and version 2, it automatically determines on a per-connection basis which of the two versions is present. diff --git a/docs/root/configuration/listeners/network_filters/geoip_filter.rst b/docs/root/configuration/listeners/network_filters/geoip_filter.rst index a9f276acfd8fb..b895489d458be 100644 --- a/docs/root/configuration/listeners/network_filters/geoip_filter.rst +++ b/docs/root/configuration/listeners/network_filters/geoip_filter.rst @@ -144,7 +144,7 @@ Database type can be one of `city_db `_, ``.total``, Counter, Total number of lookups performed for a given geolocation database file. ``.hit``, Counter, Total number of successful lookups (with non empty lookup result) performed for a given geolocation database file. - ``.lookup_error``, Counter, Total number of errors that occured during lookups for a given geolocation database file. + ``.lookup_error``, Counter, Total number of errors that occurred during lookups for a given geolocation database file. ``.db_reload_success``, Counter, Total number of times when the geolocation database file was reloaded successfully. ``.db_reload_error``, Counter, Total number of times when the geolocation database file failed to reload. ``.db_build_epoch``, Gauge, The build timestamp of the geolocation database file represented as a Unix epoch value. diff --git a/docs/root/configuration/listeners/network_filters/golang_filter.rst b/docs/root/configuration/listeners/network_filters/golang_filter.rst index 9fd2153535a0c..722218f9b5793 100644 --- a/docs/root/configuration/listeners/network_filters/golang_filter.rst +++ b/docs/root/configuration/listeners/network_filters/golang_filter.rst @@ -15,7 +15,7 @@ for more details on the filter's implementation. .. warning:: The Envoy Golang filter is designed to be run with the ``GODEBUG=cgocheck=0`` environment variable set. - This disables the cgo pointer check. + This disables the `cgo `_ pointer check. Failure to set this environment variable will cause Envoy to crash! diff --git a/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst b/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst index 5848d667488ee..f5893eb079585 100644 --- a/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst +++ b/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst @@ -35,7 +35,7 @@ be immediately closed without further filter iteration. Statistics ---------- -Every configured local rate limit filter has statistics rooted at *local_ratelimit..* +Every configured local rate limit filter has statistics rooted at *local_rate_limit..* with the following statistics: .. csv-table:: diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index c299b6ae9c449..1dace6c5215c1 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -34,6 +34,7 @@ filters. set_filter_state sni_cluster_filter sni_dynamic_forward_proxy_filter + tcp_bandwidth_limit_filter tcp_proxy_filter thrift_proxy_filter wasm_filter diff --git a/docs/root/configuration/listeners/network_filters/tcp_bandwidth_limit_filter.rst b/docs/root/configuration/listeners/network_filters/tcp_bandwidth_limit_filter.rst new file mode 100644 index 0000000000000..7008f26b1e8fa --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/tcp_bandwidth_limit_filter.rst @@ -0,0 +1,66 @@ +.. _config_network_filters_tcp_bandwidth_limit: + +TCP bandwidth limit +=================== + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.network.tcp_bandwidth_limit.v3.TcpBandwidthLimit``. +* :ref:`v3 API reference ` + +Overview +-------- + +The TCP bandwidth limit filter is a network filter that limits the bandwidth on the downstream +connection. It can be configured to limit read and write bandwidth independently, using a token +bucket algorithm similar to the HTTP bandwidth limit filter. + +- ``read_limit_kbps`` limits data read from the downstream connection. +- ``write_limit_kbps`` limits data written to the downstream connection. + +The filter works by: + +* Consuming tokens from a token bucket when data passes through +* Buffering data when insufficient tokens are available +* Using timers to refill tokens and resume data flow when bandwidth becomes available + +Example configuration +--------------------- + +The following example configuration limits read bandwidth to 1 MiB/s and write bandwidth to 512 KiB/s: + +.. code-block:: yaml + + name: envoy.filters.network.tcp_bandwidth_limit + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_bandwidth_limit.v3.TcpBandwidthLimit + stat_prefix: bandwidth_limiter + read_limit_kbps: 1024 # 1 MiB/s + write_limit_kbps: 512 # 512 KiB/s + fill_interval: + nanos: 50000000 # 50ms + +Statistics +---------- + +The TCP bandwidth limit filter outputs statistics in the ``.`` namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + read_enabled, Counter, Total number of times the read limit was applied to incoming data + write_enabled, Counter, Total number of times the write limit was applied to outgoing data + read_throttled, Counter, Total number of times read data was throttled + write_throttled, Counter, Total number of times write data was throttled + read_total_bytes, Counter, Total bytes read + write_total_bytes, Counter, Total bytes written + read_bytes_buffered, Gauge, Current number of bytes buffered for read + write_bytes_buffered, Gauge, Current number of bytes buffered for write + read_rate_bps, Gauge, Current read rate in bytes per second + write_rate_bps, Gauge, Current write rate in bytes per second + +Runtime +------- + +The TCP bandwidth limit filter can be runtime feature flagged via the :ref:`runtime_enabled +` +configuration field. diff --git a/docs/root/configuration/listeners/network_filters/tcp_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/tcp_proxy_filter.rst index afbf63c685808..84142589cd3f0 100644 --- a/docs/root/configuration/listeners/network_filters/tcp_proxy_filter.rst +++ b/docs/root/configuration/listeners/network_filters/tcp_proxy_filter.rst @@ -104,6 +104,23 @@ Example configuration: The ``ON_DOWNSTREAM_DATA`` mode is not suitable for server-first protocols where the server sends the initial greeting (e.g., SMTP, MySQL, POP3). For such protocols, use ``IMMEDIATE`` mode. +Delay Route Selection +^^^^^^^^^^^^^^^^^^^^^ + +The ``envoy.reloadable_features.tcp_proxy_delay_route_selection`` runtime guard controls whether we delay the +route selection until the filter needs to establish the upstream connection. When unset, the filter will select a +route on new connection. + +The moment the selection occurs depends on the connection mode: + +* ``IMMEDIATE`` with route selection delay: It has no effect. Route selection still happens on a new connection +* ``ON_DOWNSTREAM_DATA`` with route selection delay: Route selection will happen when the filter first receives + data from downstream +* ``ON_DOWNSTREAM_TLS_HANDSHAKE`` with route selection delay: Route selection will happen when the downstream TLS + handshake completes + +This will also delay updates to any statistics that are dependent on route selection (e.g. ``downstream_cx_no_route``). + Filter state configuration ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -210,6 +227,7 @@ The downstream statistics are rooted at *tcp..* with the following downstream_cx_total, Counter, Total number of connections handled by the filter downstream_cx_no_route, Counter, Number of connections for which no matching route was found or the cluster for the route was not found + downstream_cx_drain_close, Counter, Total number of connections closed due to drain close downstream_cx_tx_bytes_total, Counter, Total bytes written to the downstream connection downstream_cx_tx_bytes_buffered, Gauge, Total bytes currently buffered to the downstream connection downstream_cx_rx_bytes_total, Counter, Total bytes read from the downstream connection @@ -225,3 +243,4 @@ The downstream statistics are rooted at *tcp..* with the following on_demand_cluster_timeout, Counter, Total number of connections closed due to on demand cluster lookup timeout upstream_flush_total, Counter, Total number of connections that continued to flush upstream data after the downstream connection was closed upstream_flush_active, Gauge, Total connections currently continuing to flush upstream data after the downstream connection was closed + route_delayed_total, Counter, Total number of route selections that were delayed until the filter needs to establish the downstream connection diff --git a/docs/root/configuration/listeners/stats.rst b/docs/root/configuration/listeners/stats.rst index 0e93a702d0636..b16c60a102cd2 100644 --- a/docs/root/configuration/listeners/stats.rst +++ b/docs/root/configuration/listeners/stats.rst @@ -140,4 +140,5 @@ statistics. Any ``:`` character in the stats name is replaced with ``_``. total_listeners_warming, Gauge, Number of currently warming listeners. total_listeners_active, Gauge, Number of currently active listeners. total_listeners_draining, Gauge, Number of currently draining listeners. + workers_pinned, Gauge, Number of worker threads assigned a CPU when worker CPU affinity is enabled and 0 otherwise. workers_started, Gauge, A boolean (1 if started and 0 otherwise) that indicates whether listeners have been initialized on workers. diff --git a/docs/root/configuration/observability/access_log/stats.rst b/docs/root/configuration/observability/access_log/stats.rst index 5bc23c23ef67d..8b020b143430c 100644 --- a/docs/root/configuration/observability/access_log/stats.rst +++ b/docs/root/configuration/observability/access_log/stats.rst @@ -16,6 +16,8 @@ The gRPC access log has statistics rooted at *access_logs.grpc_access_log.* with logs_written, Counter, Total log entries sent to the logger which were not dropped. This does not imply the logs have been flushed to the gRPC endpoint yet. logs_dropped, Counter, Total log entries dropped due to network or application level back up. + grpc_entries_flushed, Counter, Total log entries in batches that were successfully submitted to the gRPC stream. Note that for the streaming gRPC ALS protocol there is no per-batch acknowledgement from the server; this counter reflects entries written to the gRPC send buffer. + grpc_entries_flush_failed, Counter, Total log entries in batches where the gRPC send attempt failed (stream creation failure or stream above write buffer high-watermark). An entry counted here may later be counted in ``grpc_entries_flushed`` if the batch is retried successfully on the next flush interval. File access log statistics diff --git a/docs/root/configuration/observability/stat_sinks/dynamic_modules_stat_sink.rst b/docs/root/configuration/observability/stat_sinks/dynamic_modules_stat_sink.rst new file mode 100644 index 0000000000000..d729cdf2b2435 --- /dev/null +++ b/docs/root/configuration/observability/stat_sinks/dynamic_modules_stat_sink.rst @@ -0,0 +1,43 @@ +.. _config_stat_sinks_dynamic_modules: + +Dynamic Modules Stats Sink +========================== + +The :ref:`DynamicModuleStatsSink ` +configuration specifies a stats sink backed by a :ref:`dynamic module `: +a shared object file loaded via ``dlopen`` that implements a small set of C ABI +callbacks. Envoy hands the module each periodic metric snapshot during flush +and every histogram observation synchronously, letting a module written in any +language that can produce an ELF (C, Rust, Go, etc.) ship metrics to any +backend without requiring a custom Envoy build. + +* This extension should be configured with the type URL + ``type.googleapis.com/envoy.extensions.stat_sinks.dynamic_modules.v3.DynamicModuleStatsSink``. +* :ref:`v3 API reference ` + +.. attention:: + + Dynamic modules run in-process with the same privileges as Envoy. Only + load modules you trust. This extension is currently under active + development. Capabilities and ABI are expected to evolve. + +Configuration example +--------------------- + +.. code-block:: yaml + + stats_flush_interval: 10s + + stats_sinks: + - name: envoy.stat_sinks.dynamic_modules + typed_config: + "@type": type.googleapis.com/envoy.extensions.stat_sinks.dynamic_modules.v3.DynamicModuleStatsSink + dynamic_module_config: + name: my_stats_sink + do_not_close: true + sink_name: my_sink_impl + sink_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + endpoint: "metrics.example.com:9125" + prefix: "envoy." diff --git a/docs/root/configuration/observability/stat_sinks/stat_sinks.rst b/docs/root/configuration/observability/stat_sinks/stat_sinks.rst index 503366cd8268d..9b67b90831dc3 100644 --- a/docs/root/configuration/observability/stat_sinks/stat_sinks.rst +++ b/docs/root/configuration/observability/stat_sinks/stat_sinks.rst @@ -4,7 +4,9 @@ Stat sinks .. toctree:: :maxdepth: 2 + dynamic_modules_stat_sink graphite_statsd_stat_sink kafka_stat_sink open_telemetry_stat_sink wasm_stat_sink + wasm_filter_stat_sink diff --git a/docs/root/configuration/observability/stat_sinks/wasm_filter_stat_sink.rst b/docs/root/configuration/observability/stat_sinks/wasm_filter_stat_sink.rst new file mode 100644 index 0000000000000..94f3b8682354e --- /dev/null +++ b/docs/root/configuration/observability/stat_sinks/wasm_filter_stat_sink.rst @@ -0,0 +1,224 @@ +.. _config_stat_sinks_wasm_filter: + +Wasm Stats Filter Sink +====================== + +The :ref:`WasmFilterStatsSinkConfig ` +configuration specifies a stats sink middleware that runs a +`WebAssembly `_ (WASM) plugin to filter, transform, +and enrich metrics before delegating to an inner stats sink. + +This is useful when you need custom, programmable logic to decide which metrics are +exported -- for example, dropping high-cardinality metrics by name pattern, injecting +tags from node metadata, renaming metrics, or adding synthetic metrics. It replaces +centralized metric processing services with distributed in-proxy logic. + +* This extension should be configured with the type URL + ``type.googleapis.com/envoy.extensions.stat_sinks.wasm_filter.v3.WasmFilterStatsSinkConfig``. +* :ref:`v3 API reference ` + +.. attention:: + + The Wasm stats filter sink is only included in :ref:`contrib images ` + +.. attention:: + + The Wasm stats filter sink is experimental and is currently under active development. + Capabilities will be expanded over time and the configuration structures are likely to change. + +How it works +------------ + +The ``wasm_filter`` sink wraps any existing stats sink (the *inner sink*) and +interposes a WASM plugin on each flush cycle: + +1. Envoy's flush timer fires and passes the full ``MetricSnapshot`` to the + ``wasm_filter`` sink. +2. The sink serializes all **counters** and **gauges** into a compact binary buffer + and invokes the WASM plugin's ``onStatsUpdate()`` callback. +3. The plugin iterates the metrics and applies custom logic. It may call foreign + functions to: + + * **Get additional data**: histogram names, per-metric tags + * **Set global tags**: applied to all metrics on every flush + * **Rename metrics**: override the name of specific counters/gauges/histograms + * **Inject synthetic metrics**: add new counters/gauges to the snapshot + * **Emit kept indices**: declare which metrics to keep + +4. The host builds an enriched snapshot: filtered metrics wrapped with tag/name + overrides, plus any injected synthetic metrics. +5. The enriched snapshot is flushed to the inner sink. + +**Text readouts** and **host counters/gauges** pass through unfiltered. + +Capabilities +------------ + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Capability + - Foreign Function + - Description + * - **Filter** + - ``stats_filter_emit`` + - Declare which counters, gauges, and histograms to keep. + * - **Global tag injection** + - ``stats_filter_set_global_tags`` + - Set tags (e.g. datacenter, pod) applied to ALL metrics. Called once at + startup from ``onConfigure()``. + * - **Metric renaming** + - ``stats_filter_set_name_overrides`` + - Override the name of specific metrics (e.g. prefix with ``envoy.``). + Called per flush. + * - **Synthetic metrics** + - ``stats_filter_inject_metrics`` + - Inject new counters/gauges with custom names, values, and tags. + Called per flush. + * - **Histogram discovery** + - ``stats_filter_get_histograms`` + - Get the list of histogram names (not in the standard ``onStatsUpdate`` + buffer). + * - **Per-metric tags** + - ``stats_filter_get_metric_tags`` + - Get tags for a single metric by type and index. + * - **Bulk tags** + - ``stats_filter_get_all_metric_tags`` + - Get tags for all metrics in one call. + +Configuration example +--------------------- + +.. code-block:: yaml + + stats_flush_interval: 10s + + stats_sinks: + - name: envoy.stat_sinks.wasm_filter + typed_config: + "@type": type.googleapis.com/envoy.extensions.stat_sinks.wasm_filter.v3.WasmFilterStatsSinkConfig + wasm_config: + name: "my_stats_filter" + root_id: "stats_filter" + vm_config: + runtime: "envoy.wasm.runtime.v8" + code: + local: + filename: "/etc/envoy/stats_filter_plugin.wasm" + configuration: + "@type": type.googleapis.com/google.protobuf.StringValue + value: '{"exclude_prefixes": ["server.compilation", "runtime."], "rename_with_prefix": true}' + inner_sink: + name: envoy.stat_sinks.kafka + typed_config: + "@type": type.googleapis.com/envoy.extensions.stat_sinks.kafka.v3.KafkaStatsSinkConfig + broker_list: "kafka:9092" + topic: "envoy-metrics" + format: PROTOBUF + emit_tags_as_labels: true + +Wire format reference +--------------------- + +``stats_filter_emit`` +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + [counter_count] [counter_idx_0] ... [counter_idx_N] + [gauge_count] [gauge_idx_0] ... [gauge_idx_M] + [hist_count] [hist_idx_0] ... [hist_idx_K] (optional) + +``stats_filter_set_global_tags`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + [tag_count: uint32] + For each tag: + [name_len: uint32] [name bytes] [value_len: uint32] [value bytes] + +``stats_filter_set_name_overrides`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + [count: uint32] + For each override: + [type: uint32 (1=counter, 2=gauge, 3=histogram)] + [index: uint32] + [new_name_len: uint32] [new_name bytes] + +``stats_filter_inject_metrics`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + [counter_count: uint32] + For each counter: + [name_len: uint32] [name bytes] [value: uint64] + [tag_count: uint32] for each tag: [name_len][name][value_len][value] + [gauge_count: uint32] + For each gauge: + [name_len: uint32] [name bytes] [value: uint64] + [tag_count: uint32] for each tag: [name_len][name][value_len][value] + +``stats_filter_get_histograms`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Input**: empty. **Output**: + +.. code-block:: text + + [count: uint32] for each: [name_len: uint32] [name bytes] + +``stats_filter_get_metric_tags`` (single) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Input**: ``[type: uint32] [index: uint32]``. **Output**: + +.. code-block:: text + + [tag_count: uint32] + For each tag: [name_len: uint32] [name] [value_len: uint32] [value] + +``stats_filter_get_all_metric_tags`` (bulk) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Input**: empty. **Output**: three consecutive blocks (counters, gauges, histograms): + +.. code-block:: text + + [metric_count: uint32] + For each metric: + [tag_count: uint32] + For each tag: [name_len: uint32] [name] [value_len: uint32] [value] + +Reading node metadata +--------------------- + +The plugin can read Envoy's node metadata during ``onConfigure()`` using +``getProperty()`` / ``getValue()`` with paths like: + +* ``["xds", "node", "id"]`` -- node ID (hostname) +* ``["xds", "node", "cluster"]`` -- cluster name +* ``["xds", "node", "metadata", "datacenter"]`` -- custom metadata +* ``["xds", "node", "locality", "region"]`` -- locality region + +These are used to compute global tags via ``stats_filter_set_global_tags``. + +Performance considerations +-------------------------- + +* **Global tags**: set once at startup, zero per-flush overhead. +* **Metric filtering**: single WASM boundary crossing for the serialized buffer. +* **Bulk tag lookup**: ``stats_filter_get_all_metric_tags`` -- one crossing vs N. +* **Name overrides and injection**: one crossing each per flush. + +Current limitations +------------------- + +* **Text readouts are not filterable** -- they pass through to the inner sink. +* **Value transformation is not supported** -- the plugin can rename and tag + metrics but cannot modify counter/gauge values. diff --git a/docs/root/configuration/observability/statistics.rst b/docs/root/configuration/observability/statistics.rst index 085051705b931..ead539139113c 100644 --- a/docs/root/configuration/observability/statistics.rst +++ b/docs/root/configuration/observability/statistics.rst @@ -30,8 +30,8 @@ Server related statistics are rooted at *server.* with following statistics: hot_restart_generation, Gauge, Current hot restart generation -- like hot_restart_epoch but computed automatically by incrementing from parent. initialization_time_ms, Histogram, Total time taken for Envoy initialization in milliseconds. This is the time from server start-up until the worker threads are ready to accept new connections debug_assertion_failures, Counter, Number of debug assertion failures detected in a release build if compiled with ``--define log_debug_assert_in_release=enabled`` or zero otherwise - envoy_bug_failures, Counter, Number of envoy bug failures detected in a release build. File or report the issue if this increments as this may be serious. - envoy_notifications, Counter, Number of envoy notifications detected. File or report the issue if this increments as this may be serious. Please include logs from the ``notification`` component at the debug level. See :ref:`command line option --component-log-level ` for details. + envoy_bug_failures, Counter, Number of Envoy bug failures detected in a release build. File or report the issue if this increments as this may be serious. + envoy_notifications, Counter, Number of Envoy notifications detected. File or report the issue if this increments as this may be serious. Please include logs from the ``notification`` component at the debug level. See :ref:`command line option --component-log-level ` for details. static_unknown_fields, Counter, Number of messages in static configuration with unknown fields dynamic_unknown_fields, Counter, Number of messages in dynamic configuration with unknown fields wip_protos, Counter, Number of messages and fields marked as work-in-progress being used @@ -50,4 +50,4 @@ Server Compilation Settings related statistics are rooted at *server.compilation :header: Name, Type, Description :widths: 1, 1, 2 - fips_mode, Gauge, Integer representing whether the envoy build is FIPS compliant or not + fips_mode, Gauge, Integer representing whether the Envoy build is FIPS compliant or not diff --git a/docs/root/configuration/operations/overload_manager/overload_manager.rst b/docs/root/configuration/operations/overload_manager/overload_manager.rst index 73acf6b58b989..ec28f16ce150c 100644 --- a/docs/root/configuration/operations/overload_manager/overload_manager.rst +++ b/docs/root/configuration/operations/overload_manager/overload_manager.rst @@ -149,6 +149,12 @@ The following overload actions are supported: - Envoy will reset expensive streams to terminate them. See :ref:`below ` for details on configuration. + * - envoy.overload_actions.close_idle_http_connections + - Envoy will close idle downstream HTTP/3 QUIC connections when the action is active. + When the action is *saturated*, connections will be closed aggressively (ignoring the idle timer threshold). + When the action is in a *scaled active* state, the idle timer threshold is still respected. + Note that this action is currently only supported for HTTP/3 QUIC connections. + .. _config_overload_manager_shrink_heap: Shrink Heap @@ -227,7 +233,7 @@ The following core load shed points are supported: * - envoy.load_shed_points.http_connection_manager_decode_headers - Envoy will reject new HTTP streams by sending a local reply. This occurs - right after the http codec has finished parsing headers but before the + right after the HTTP codec has finished parsing headers but before the :ref:`HTTP Filter Chain is instantiated `. * - envoy.load_shed_points.http1_server_abort_dispatch diff --git a/docs/root/configuration/operations/tools/router_check.rst b/docs/root/configuration/operations/tools/router_check.rst index b02522e30c8b6..d553254a0f683 100644 --- a/docs/root/configuration/operations/tools/router_check.rst +++ b/docs/root/configuration/operations/tools/router_check.rst @@ -165,7 +165,7 @@ input ssl *(optional, boolean)* A flag that determines whether to set x-forwarded-proto to https or http. By setting x-forwarded-proto to a given protocol, the tool is able to simulate the behavior of - a client issuing a request via http or https. By default ssl is false which corresponds to + a client issuing a request via HTTP or HTTPS. By default ssl is false which corresponds to x-forwarded-proto set to http. runtime diff --git a/docs/root/configuration/other_features/_include/dlb.yaml b/docs/root/configuration/other_features/_include/dlb.yaml deleted file mode 100644 index 5a5081b7ff73b..0000000000000 --- a/docs/root/configuration/other_features/_include/dlb.yaml +++ /dev/null @@ -1,7 +0,0 @@ -static_resources: - listeners: - - connection_balance_config: - extend_balance: - name: envoy.network.connection_balance.dlb - typed_config: - "@type": type.googleapis.com/envoy.extensions.network.connection_balance.dlb.v3alpha.Dlb diff --git a/docs/root/configuration/other_features/dlb.rst b/docs/root/configuration/other_features/dlb.rst deleted file mode 100644 index 5768b633c8a46..0000000000000 --- a/docs/root/configuration/other_features/dlb.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. _config_connection_balance_dlb: - -DLB Connection Balancer -======================= - -* :ref:`v3 API reference ` - - -This connection balancer extension provides Envoy with low latency networking by integrating with `Intel DLB `_ through the libdlb library. - -The DLB connection balancer is only included in :ref:`contrib images `. - -Example configuration ---------------------- - -An example for DLB connection balancer configuration is: - -.. literalinclude:: _include/dlb.yaml - :language: yaml - - -How it works ------------- - -If enabled, the DLB connection balancer will: - -- attach DLB hardware -- create a queue for balancing -- create one port to send and one port to receive for each worker thread -- create one eventfd for each worker thread and attach each eventfd to corresponding customer -- register each eventfd to corresponding customer and DLB hardware - -When new connections come, one worker thread will accept it and send it to DLB hardware. DLB hardware -does balancing then trigger one worker thread to receive via libevent. - -Installing DLB --------------- - -For information on how to build/install and use libdlb see `the getting started guide `_. diff --git a/docs/root/configuration/other_features/other_features.rst b/docs/root/configuration/other_features/other_features.rst index 28683a97a9e6f..0f9888a482cfd 100644 --- a/docs/root/configuration/other_features/other_features.rst +++ b/docs/root/configuration/other_features/other_features.rst @@ -5,13 +5,13 @@ Other features :maxdepth: 2 bootstrap_extensions/dynamic_modules - dlb hyperscan internal_listener rate_limit reverse_tunnel io_uring vcl + sockmap wasm wasm_service qatzip diff --git a/docs/root/configuration/other_features/reverse_tunnel.rst b/docs/root/configuration/other_features/reverse_tunnel.rst index 75a01271d1d42..c1f56c32de37d 100644 --- a/docs/root/configuration/other_features/reverse_tunnel.rst +++ b/docs/root/configuration/other_features/reverse_tunnel.rst @@ -67,9 +67,9 @@ are reachable through the reverse tunnel. .. literalinclude:: /_configs/reverse_connection/initiator-envoy.yaml :language: yaml - :lines: 17-50 + :lines: 33-66 :linenos: - :lineno-start: 17 + :lineno-start: 33 :caption: :download:`initiator-envoy.yaml ` The special ``rc://`` address format encodes connection and identity metadata: @@ -94,9 +94,9 @@ The ``downstream-service`` cluster in the example refers to the service behind t .. literalinclude:: /_configs/reverse_connection/initiator-envoy.yaml :language: yaml - :lines: 69-80 + :lines: 85-96 :linenos: - :lineno-start: 69 + :lineno-start: 85 :caption: :download:`initiator-envoy.yaml ` Upstream cluster @@ -108,9 +108,9 @@ This cluster can be defined statically in the bootstrap configuration or added d .. literalinclude:: /_configs/reverse_connection/initiator-envoy.yaml :language: yaml - :lines: 54-65 + :lines: 70-81 :linenos: - :lineno-start: 54 + :lineno-start: 70 :caption: :download:`initiator-envoy.yaml ` Multiple cluster support @@ -215,6 +215,69 @@ concatenates the tenant identifier with the node and cluster identifiers using t in any of the reverse tunnel headers are rejected with ``400`` to prevent ambiguous lookups. The flag defaults to ``false`` to preserve existing behaviour. +Lifecycle access logs +~~~~~~~~~~~~~~~~~~~~~ + +The upstream socket interface can emit access logs for reverse-tunnel lifecycle events directly from +reverse-tunnel-owned code. Configure the ``access_log`` field on +``envoy.bootstrap.reverse_tunnel.upstream_socket_interface`` to log tunnel setup, socket handoff, +tunnel close, and post-handoff HTTP/2 keepalive timeout events: + +.. literalinclude:: /_configs/reverse_connection/responder-envoy.yaml + :language: yaml + :lines: 7-24 + :linenos: + :lineno-start: 7 + :caption: :download:`responder-envoy.yaml ` + +The lifecycle logger emits the following event names: + +**Core lifecycle events:** + +* ``tunnel_setup`` – emitted when a new reverse tunnel connection is accepted and cached. +* ``socket_handoff`` – emitted when a cached idle socket is handed off to an upstream connection pool. +* ``tunnel_closed`` – emitted when the reverse tunnel connection is closed. +* ``http2_keepalive_timeout`` – emitted when the upstream connection closes locally due to an HTTP/2 keepalive (``PING``) timeout after handoff. + +**Idle-phase ping events** (emitted while the socket is idle in the cache): + +* ``idle_ping_sent`` – an ``RPING`` probe was sent to verify the idle connection is alive. +* ``idle_ping_ack`` – the peer acknowledged the ``RPING``. +* ``idle_ping_miss`` – no ``RPING`` acknowledgement was received within the expected window. +* ``idle_ping_timeout`` – the idle connection is being closed because the peer failed to respond to ``RPING`` probes. + +The dynamic metadata namespace is ``envoy.reverse_tunnel.lifecycle``. The emitted fields are +``event``, ``node_id``, ``cluster_id``, ``tenant_id``, ``worker``, ``fd``, ``socket_state``, +and, when relevant, ``handoff_kind`` or ``close_reason``. + +**Socket state values** (the ``socket_state`` field): + +* ``idle`` – the socket is cached and waiting for a data request. +* ``handed_off`` – the socket has been handed off to an upstream connection pool. +* ``in_use`` – the socket is actively being used for upstream traffic. + +**Handoff kind values** (the ``handoff_kind`` field, present only in ``socket_handoff`` events): + +* ``pool_to_upstream`` – the socket was handed off from the idle cache to the upstream connection pool. + +**Close reason values** (the ``close_reason`` field, present only in ``tunnel_closed`` events): + +* ``idle_peer_close`` – the peer closed the connection while it was idle. +* ``idle_read_error`` – a read error occurred on the idle connection. +* ``idle_ping_write_failure`` – writing an ``RPING`` probe to the idle connection failed. +* ``idle_ping_timeout`` – the idle connection was closed because ``RPING`` probes went unanswered. +* ``remote_close`` – the peer closed the connection after handoff. +* ``local_close`` – Envoy closed the connection after handoff. +* ``explicit_close`` – the connection was explicitly closed (e.g., during shutdown). + +The same identifiers are also copied into connection filter state under these keys: + +* ``envoy.reverse_tunnel.node_id`` +* ``envoy.reverse_tunnel.cluster_id`` +* ``envoy.reverse_tunnel.tenant_id`` +* ``envoy.reverse_tunnel.worker`` +* ``envoy.reverse_tunnel.fd`` + .. _config_reverse_tunnel_network_filter: Reverse tunnel network filter @@ -226,9 +289,9 @@ parameters. .. literalinclude:: /_configs/reverse_connection/responder-envoy.yaml :language: yaml - :lines: 17-28 + :lines: 29-41 :linenos: - :lineno-start: 17 + :lineno-start: 29 :caption: :download:`responder-envoy.yaml ` .. _config_reverse_connection_cluster: @@ -258,11 +321,26 @@ automatically constructs tenant-scoped identifiers using the formatted tenant ID will not be routed. This ensures strict tenant isolation and prevents requests from being routed without proper tenant scoping. +To observe post-handoff upstream connection events such as HTTP/2 keepalive timeout, add the reverse +tunnel lifecycle upstream network filter to the reverse connection cluster: + .. literalinclude:: /_configs/reverse_connection/responder-envoy.yaml :language: yaml - :lines: 92-112 + :lines: 105-111 :linenos: - :lineno-start: 92 + :lineno-start: 105 + :caption: :download:`responder-envoy.yaml ` + +This filter copies the reverse-tunnel identifiers into the handed-off upstream connection's filter +state and emits exactly one ``http2_keepalive_timeout`` access-log event when the upstream +connection closes locally with ``http2_ping_timeout``. The subsequent ``tunnel_closed`` record +reuses the same close reason. + +.. literalinclude:: /_configs/reverse_connection/responder-envoy.yaml + :language: yaml + :lines: 104-129 + :linenos: + :lineno-start: 104 :caption: :download:`responder-envoy.yaml ` The reverse connection cluster configuration includes several key fields: @@ -311,9 +389,9 @@ that identifies the target downstream node for each request. .. literalinclude:: /_configs/reverse_connection/responder-envoy.yaml :language: yaml - :lines: 31-88 + :lines: 43-101 :linenos: - :lineno-start: 31 + :lineno-start: 43 :caption: :download:`responder-envoy.yaml ` The example above demonstrates using a :ref:`Lua filter ` to implement flexible @@ -372,6 +450,104 @@ The header priority order is: be inferred from the request (e.g., the ``x-tenant-id`` header is missing or the formatter evaluates to empty), host selection will fail and the request will not be routed. +.. _config_reverse_tunnel_access_logging: + +Access logging +-------------- + +Both the initiator and responder bootstrap extensions support access logging for reverse tunnel +lifecycle events. Access logs are emitted at key connection lifecycle points, providing visibility +into tunnel establishment, handshake outcomes, and connection teardown. + +Initiator access logging +~~~~~~~~~~~~~~~~~~~~~~~~ + +The initiator (downstream) Envoy can be configured to log reverse tunnel lifecycle events by adding +an ``access_log`` field to the downstream socket interface bootstrap extension: + +.. literalinclude:: /_configs/reverse_connection/initiator-envoy.yaml + :language: yaml + :lines: 7-28 + :linenos: + :lineno-start: 7 + :caption: :download:`initiator-envoy.yaml ` + +Any :ref:`access log ` type supported by Envoy (file, stdout, gRPC, etc.) +can be used. The access log configuration follows the same format as access logs in other Envoy +components such as the :ref:`TCP proxy ` and +:ref:`HTTP connection manager `. + +Initiator lifecycle events +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The initiator emits access log entries at the following lifecycle points: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Event + - Description + * - ``handshake_success`` + - A reverse tunnel handshake completed successfully. The connection is now established + and available for data requests from the responder. + * - ``handshake_failure`` + - A reverse tunnel handshake failed. The ``error`` field contains the failure reason + (e.g., HTTP status error, encode error, connection closed). + * - ``connection_closed`` + - An established reverse tunnel connection was closed. This triggers re-establishment + on the next maintenance cycle. + +Initiator dynamic metadata fields +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All initiator access log fields are available under the ``envoy.reverse_tunnel.initiator`` dynamic +metadata namespace and can be referenced using the ``%DYNAMIC_METADATA(envoy.reverse_tunnel.initiator:FIELD)%`` +:ref:`format string `. + +.. list-table:: + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Type + - Description + * - ``event`` + - string + - The lifecycle event that triggered this log entry. One of: ``handshake_success``, + ``handshake_failure``, ``connection_closed``. + * - ``node_id`` + - string + - The ``src_node_id`` of this initiator Envoy instance, as configured in the ``rc://`` + listener address. + * - ``cluster_id`` + - string + - The ``src_cluster_id`` of this initiator Envoy instance, as configured in the ``rc://`` + listener address. + * - ``tenant_id`` + - string + - The ``src_tenant_id`` of this initiator Envoy instance, as configured in the ``rc://`` + listener address. Empty if tenant isolation is not used. + * - ``upstream_cluster`` + - string + - The name of the upstream cluster that this reverse tunnel connects to. + * - ``host_address`` + - string + - The resolved address of the specific upstream host that this connection targets. + * - ``connection_key`` + - string + - A unique identifier for this specific reverse tunnel connection instance. Useful for + correlating handshake and close events for the same connection. + * - ``error`` + - string + - The error message describing the failure reason. Empty string on non-failure events. + Populated on ``handshake_failure`` events with the failure reason, e.g., + ``HTTP handshake failed with status 401``, ``HTTP handshake encode failed``, + ``Connection closed``. + +In addition to dynamic metadata fields, standard Envoy access log format strings such as +``%START_TIME%``, ``%DURATION%``, and ``%CONNECTION_TERMINATION_DETAILS%`` are also available. + .. _config_reverse_connection_security: Security considerations diff --git a/docs/root/configuration/other_features/sockmap.rst b/docs/root/configuration/other_features/sockmap.rst new file mode 100644 index 0000000000000..745a726473b67 --- /dev/null +++ b/docs/root/configuration/other_features/sockmap.rst @@ -0,0 +1,119 @@ +.. _config_sock_interface_sockmap: + +Sockmap socket interface +======================== + +* :ref:`v3 API reference ` + +.. attention:: + + The sockmap socket interface extension is experimental and is currently under active development. + +.. note:: + + This feature is only supported on Linux and requires a kernel 4.18 or later. + +The sockmap socket interface accelerates same-host TCP hops by loading eBPF ``sock_ops`` and +``sk_msg`` programs that redirect payloads between local sockets through a +``BPF_MAP_TYPE_SOCKHASH``, bypassing the kernel TCP/IP stack. Connections whose peer is not on the +same host are absent from the map and transparently fall back to TCP/IP, so behavior is unchanged +for traffic that cannot be accelerated. + +Loading and attaching the eBPF programs requires ``CAP_SYS_ADMIN``, or ``CAP_BPF`` and +``CAP_NET_ADMIN`` on newer kernels. When the programs cannot be loaded or attached, the interface +logs the failure and every socket falls back to the standard datapath, so traffic is never +interrupted. + +Building Envoy with sockmap support +----------------------------------- + +The eBPF datapath is only compiled when Envoy is built with ``--define=sockmap=enabled``, which +links ``libbpf``: + +.. code-block:: bash + + bazel build --define=sockmap=enabled //source/exe:envoy-static + +Default builds compile a no-op stub instead. The extension is still registered, so configuring +``bpf_program_path`` in a default build logs a warning and leaves every socket on the standard +datapath. + +Compiling the eBPF object +------------------------- + +Envoy does not ship a compiled eBPF object. The ``sock_ops`` and ``sk_msg`` programs and the user +space registration must share the same map name and key layout. The program source is part of this +extension at +:repo:`sockmap_kern.c `, and +Envoy provides a build rule that compiles it into ``sockmap_kern.o`` with ``clang``: + +.. code-block:: bash + + bazel build //source/extensions/network/socket_interface/sockmap:sockmap_bpf + +Compiling the object requires ``clang`` and the ``libbpf`` development headers on the host. Point +:ref:`bpf_program_path +` at the +resulting object, or at a custom build that exports the ``envoy_sockops`` and ``envoy_sk_msg`` +programs and the ``envoy_sockhash`` map under those names with a matching key layout. + +Example configuration +--------------------- + +Register the socket interface as a bootstrap extension and select it as the default socket +interface: + +.. code-block:: yaml + + bootstrap_extensions: + - name: envoy.extensions.network.socket_interface.sockmap + typed_config: + "@type": type.googleapis.com/envoy.extensions.network.socket_interface.sockmap.v3.Sockmap + bpf_program_path: /etc/envoy/sockmap_kern.o + default_socket_interface: "envoy.extensions.network.socket_interface.sockmap" + +The example accelerates proxy-to-proxy hops, which is the default. Accelerating application-to-proxy +hops additionally requires setting ``cgroup_path``. + +How it works +------------ + +When ``bpf_program_path`` points at an object Envoy can load, the interface accelerates two kinds +of same-host hops, which can be enabled independently. + +Application-to-proxy hops +~~~~~~~~~~~~~~~~~~~~~~~~~ + +When :ref:`cgroup_path +` is set, +Envoy attaches the ``sock_ops`` program to that cgroup v2 directory. Every socket that reaches the +established state inside the cgroup is added to the ``sockhash``, which accelerates hops between +applications and Envoy that run in the same cgroup. Prefer a narrowly scoped cgroup over a broad +one such as the root. When the cgroup must be broad, set :ref:`accelerated_ports +` to the +proxy listener port ranges so only connections to or from those ports are registered, leaving +unrelated same-host connections in the cgroup on the standard datapath. If ``cgroup_path`` is not +set, the ``sock_ops`` program is not attached and application sockets are not tracked. + +Proxy-to-proxy hops +~~~~~~~~~~~~~~~~~~~ + +When :ref:`register_user_space_sockets +` +is ``true``, which is the default, Envoy registers its accepted, connected, and duplicated sockets +into the ``sockhash`` from user space. This is independent of ``cgroup_path`` and accelerates +proxy-to-proxy hops on the same host without attaching the ``sock_ops`` program. The matching entry +is removed when the socket closes, so a later connection that reuses the tuple is never redirected +into a stale entry. + +For either path, the ``sk_msg`` verdict program looks up the peer of each send in the ``sockhash`` +and, when the peer is present, redirects the payload straight to its ingress queue with +``bpf_msg_redirect_hash``. Only IPv4 stream sockets are accelerated. Other sockets, including IPv6 +and Unix domain sockets, use the standard datapath unchanged. The ``sockhash`` holds one entry per +accelerated socket, up to :ref:`sockhash_max_entries +`. + +.. note:: + + When the programs load successfully, the interface logs ``sockmap acceleration enabled using + `` at the ``info`` level. diff --git a/docs/root/configuration/other_features/wasm.rst b/docs/root/configuration/other_features/wasm.rst index 25c23c7ccf6e3..2e77e24b2adc0 100644 --- a/docs/root/configuration/other_features/wasm.rst +++ b/docs/root/configuration/other_features/wasm.rst @@ -24,3 +24,4 @@ Wasm runtime emits the following statistics: wasm..created, Counter, Total number of execution instances created wasm..active, Gauge, Number of active execution instances + wasm.wasm_vm_count, Gauge, "Process-wide number of active Wasm VMs, across every runtime" diff --git a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst index e7b6153e405d3..d9cd684476c08 100644 --- a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst +++ b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst @@ -79,6 +79,7 @@ Every cluster has a statistics tree rooted at *cluster..* with the followi upstream_rq_total, Counter, Total requests upstream_rq_active, Gauge, Total active requests upstream_rq_pending_total, Counter, Total requests pending a connection pool connection + upstream_rq_active_overflow, Counter, Total requests rejected because the ``max_requests`` circuit breaker was exhausted while attaching to a ready upstream connection (see ``envoy.reloadable_features.skip_pending_overflow_count_on_active_rq``) upstream_rq_pending_overflow, Counter, Total requests that overflowed connection pool or requests (mainly for HTTP/2 and above) circuit breaking and were failed upstream_rq_pending_failure_eject, Counter, Total requests that were failed due to a connection pool connection failure or remote connection termination upstream_rq_pending_active, Gauge, Total active requests pending a connection pool connection @@ -88,7 +89,8 @@ Every cluster has a statistics tree rooted at *cluster..* with the followi upstream_rq_timeout, Counter, Total requests that timed out waiting for a response upstream_rq_max_duration_reached, Counter, Total requests closed due to max duration reached upstream_rq_per_try_timeout, Counter, Total requests that hit the per try timeout (except when request hedging is enabled) - upstream_rq_rx_reset, Counter, Total requests that were reset remotely + upstream_rq_rx_reset, Counter, Total requests that were reset remotely with an error + upstream_rq_rx_reset_no_error, Counter, Total requests that were reset remotely with no error upstream_rq_tx_reset, Counter, Total requests that were reset locally upstream_rq_retry, Counter, Total request retries upstream_rq_retry_backoff_exponential, Counter, Total retries using the exponential backoff strategy diff --git a/docs/root/configuration/upstream/health_checkers/dynamic_modules.rst b/docs/root/configuration/upstream/health_checkers/dynamic_modules.rst new file mode 100644 index 0000000000000..62f9f38c8e61e --- /dev/null +++ b/docs/root/configuration/upstream/health_checkers/dynamic_modules.rst @@ -0,0 +1,31 @@ +.. _config_health_checkers_dynamic_modules: + +Dynamic modules +=============== + +The dynamic modules health checker is a custom health checker (with :code:`envoy.health_checkers.dynamic_modules` +as name) that delegates the actual health check to a :ref:`dynamic module `. +Envoy drives the standard per-host :ref:`interval `, +:ref:`timeout ` and +:ref:`healthy `/ +:ref:`unhealthy ` thresholds. On each interval +the module performs the check (optionally on its own thread) and reports the host's health status back to Envoy, +which applies it on the main thread. + +An example setting for :ref:`custom_health_check ` +as a dynamic modules health checker is shown below: + +.. code-block:: yaml + + custom_health_check: + name: envoy.health_checkers.dynamic_modules + typed_config: + "@type": type.googleapis.com/envoy.extensions.health_checkers.dynamic_modules.v3.DynamicModuleHealthCheck + dynamic_module_config: + name: my_health_checker_module + health_checker_name: my_health_checker + health_checker_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: example-config + +* :ref:`v3 API reference ` diff --git a/docs/root/configuration/upstream/health_checkers/health_checkers.rst b/docs/root/configuration/upstream/health_checkers/health_checkers.rst index 61b50009af147..a8a2d5e29582c 100644 --- a/docs/root/configuration/upstream/health_checkers/health_checkers.rst +++ b/docs/root/configuration/upstream/health_checkers/health_checkers.rst @@ -8,3 +8,4 @@ Health checkers redis thrift + dynamic_modules diff --git a/docs/root/faq/configuration/timeouts.rst b/docs/root/faq/configuration/timeouts.rst index c043021f215ef..d36498d0c7f28 100644 --- a/docs/root/faq/configuration/timeouts.rst +++ b/docs/root/faq/configuration/timeouts.rst @@ -57,7 +57,12 @@ Connection timeouts apply to the entire HTTP connection and all streams the conn lifetime in this scenario prevents holding onto healthy connections even after they would otherwise be undiscoverable. To modify the max connection duration for downstream connections use the :ref:`common_http_protocol_options ` - field in the HTTP connection manager configuration. To modify the max connection duration for upstream connections use the + field in the HTTP connection manager configuration. Optionally, :ref:`max_connection_duration_jitter + ` (downstream + only) can extend the effective duration by a random amount up to + ``max_connection_duration * jitter / 100``, preventing a thundering-herd of reconnects when many + connections are established at roughly the same time. To modify the max connection duration for + upstream connections use the :ref:`common_http_protocol_options ` field in the cluster configuration. * The HTTP connection manager :ref:`drain_timeout @@ -72,7 +77,11 @@ Connection timeouts apply to the entire HTTP connection and all streams the conn connection hits the :ref:`idle_timeout `, when :ref:`max_connection_duration ` is reached, or during general server draining. The default grace period is *5000 milliseconds (5 seconds)* - if this option is not specified. + if this option is not specified. Optionally, :ref:`drain_timeout_jitter + ` + can extend the grace period by a random amount up to ``drain_timeout * jitter / 100``, staggering + the final ``GOAWAY`` across simultaneously-draining connections to mitigate thundering-herd + reconnects. See :ref:`below ` for other connection timeouts. @@ -126,9 +135,21 @@ stream timeouts already introduced above. .. attention:: - This timeout defaults to *15 seconds*, however, it is not compatible with streaming responses - (responses that never end), and will need to be disabled. Stream idle timeouts should be used - in the case of streaming APIs as described elsewhere on this page. + This timeout defaults to *15 seconds*. This is typically a problem for streaming responses + (or any responses that are not expected to end within 15 seconds), and will need to be + disabled by setting to 0, or set to a long enough value. Typically + :ref:`idle_timeout ` + should be used in the case of streaming APIs as described elsewhere on this page. + + .. attention:: + + This timeout will not be triggered by streaming *requests*, as it only begins when the + request is complete. If you wish to have a timeout that applies to streaming requests, + it is recommended to set ``timeout`` to 0 to disable it, and use + :ref:`max_stream_duration ` + and/or :ref:`idle_timeout `, + whose timers start when the request begins. + * The route :ref:`idle_timeout ` allows overriding of the HTTP connection manager :ref:`stream_idle_timeout ` diff --git a/docs/root/faq/debugging/how_to_dump_heap_profile_of_envoy.rst b/docs/root/faq/debugging/how_to_dump_heap_profile_of_envoy.rst index 4f5b60624fd8b..cb551623a6bd4 100644 --- a/docs/root/faq/debugging/how_to_dump_heap_profile_of_envoy.rst +++ b/docs/root/faq/debugging/how_to_dump_heap_profile_of_envoy.rst @@ -8,7 +8,8 @@ for how to generate Envoy heap profiles. For any Envoy binary that build with normal ``tcmalloc``, the :ref:`/heap_dump ` endpoint -is supported to dump current heap profile of Envoy. +is supported to dump current heap profile of Envoy. The :ref:`/peak_heap_dump ` +endpoint is also available to dump the peak heap profile, capturing the heap state at peak memory usage. Use following Envoy process as a specific example: diff --git a/docs/root/intro/arch_overview/advanced/attributes.rst b/docs/root/intro/arch_overview/advanced/attributes.rst index 6c842cd86338d..4041b548fee87 100644 --- a/docs/root/intro/arch_overview/advanced/attributes.rst +++ b/docs/root/intro/arch_overview/advanced/attributes.rst @@ -113,7 +113,7 @@ RBAC): destination.address, string, Downstream connection local address destination.port, int, Downstream connection local port connection.id, uint, Downstream connection ID - connection.mtls, bool, Indicates whether TLS is applied to the downstream connection and the peer ceritificate is presented + connection.mtls, bool, Indicates whether TLS is applied to the downstream connection and the peer certificate is presented connection.requested_server_name, string, Requested server name in the downstream TLS connection connection.tls_version, string, TLS version of the downstream TLS connection connection.subject_local_certificate, string, The subject field of the local certificate in the downstream TLS connection @@ -123,6 +123,8 @@ RBAC): connection.uri_san_local_certificate, string, The first URI entry in the SAN field of the local certificate in the downstream TLS connection connection.uri_san_peer_certificate, string, The first URI entry in the SAN field of the peer certificate in the downstream TLS connection connection.sha256_peer_certificate_digest, string, SHA256 digest of the peer certificate in the downstream TLS connection if present + connection.peer_certificate, string, PEM-encoded peer certificate in the downstream TLS connection if present + connection.peer_certificate_valid, bool, Indicates whether the peer certificate in the downstream TLS connection was presented and validated connection.transport_failure_reason, string, The transport failure reason e.g. certificate validation failed The following additional attributes are available upon the downstream connection termination: @@ -153,11 +155,13 @@ The following attributes are available once the upstream connection is establish upstream.uri_san_local_certificate, string, The first URI entry in the SAN field of the local certificate in the upstream TLS connection upstream.uri_san_peer_certificate, string, The first URI entry in the SAN field of the peer certificate in the upstream TLS connection upstream.sha256_peer_certificate_digest, string, SHA256 digest of the peer certificate in the upstream TLS connection if present + upstream.peer_certificate, string, PEM-encoded peer certificate in the upstream TLS connection if present upstream.local_address, string, The local address of the upstream connection upstream.transport_failure_reason, string, The upstream transport failure reason e.g. certificate validation failed upstream.request_attempt_count, uint, The count of upstream request attempts. A value of ‘0’ indicates that the request was never attempted upstream upstream.cx_pool_ready_duration, duration, Total duration from when the upstream request was created to when the upstream connection pool is ready upstream.locality, :ref:`Locality`, Locality information of upstream host + upstream.server_name, string, The SNI used for the upstream TLS connection Metadata and filter state ------------------------- diff --git a/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst b/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst index 9e39057fabd53..50456356cd439 100644 --- a/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst +++ b/docs/root/intro/arch_overview/advanced/data_sharing_between_filters.rst @@ -87,8 +87,8 @@ connection and HTTP stream (i.e., HTTP request/response pair) respectively. ``StreamInfo`` contains a set of fixed attributes as part of the class definition (e.g., HTTP protocol, requested server name, etc.). In addition, it provides a facility to store typed objects in a map -(``map``). The state stored per filter can be -either write-once (immutable), or write-many (mutable). +(``map``). The state stored per filter is +write-many (mutable). See :ref:`the well-known dynamic metadata ` and :ref:`the well-known filter state ` for the reference diff --git a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst index 9abc621c74879..11b2e729e13dd 100644 --- a/docs/root/intro/arch_overview/advanced/dynamic_modules.rst +++ b/docs/root/intro/arch_overview/advanced/dynamic_modules.rst @@ -26,14 +26,18 @@ Currently, dynamic modules are supported at the following extension points: * As a :ref:`listener filter `. * As a :ref:`UDP listener filter `. * As an :ref:`access logger `. +* As a :ref:`formatter `. +* As a :ref:`stats sink `. * As a :ref:`network filter `. * As an :ref:`HTTP filter `. * As an :ref:`HTTP matching data input `. * As an :ref:`input matcher `. * As a :ref:`TLS certificate validator `. +* As a :ref:`transport socket `. * As a :ref:`load balancing policy `. * As an :ref:`upstream HTTP TCP bridge `. * As a :ref:`tracer `. +* As a :ref:`health checker `. There are a few design goals for the dynamic modules: @@ -85,12 +89,39 @@ and returns a fail-closed default: * Network filter callbacks close the connection and return ``StopIteration``. * Listener filter callbacks close the socket and return ``StopIteration``. -When ``CatchUnwind`` is applied to a filter, this prevents a single panicking module -from aborting the entire Envoy process. The affected request or connection is -terminated; other traffic is unaffected. +The SDK always guards each callback at the FFI boundary so a panic can never unwind into +Envoy and corrupt the process, regardless of whether ``CatchUnwind`` is used. The wrapper +adds graceful filter-level teardown on top of that guard. The affected request or +connection is terminated. Other traffic is unaffected. Getting started -------------------------- We have a dedicated repository for the dynamic module examples to help you get started. The repository is available at `envoyproxy/dynamic-modules-examples `_ + +Statistics +--------------------------- + +All dynamic-module extension types emit the following statistics in the shared ``dynamic_modules.`` namespace. +These stats track failures encountered while loading the extension's configuration. Each one is tagged with +``config_name``, set to the configured name of the dynamic-module extension instance — for example the +:ref:`filter_name +` +for the HTTP filter, ``transport_socket_name`` for the transport socket, ``lb_policy_name`` for the +load-balancing policy, ``tracer_name`` for the tracer or ``cluster_name`` for the cluster +(``default`` if the extension has no per-instance name, as for the UDP listener filter). + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + module_load_error, Counter, "Total dynamic modules that could not be loaded (missing or invalid module source, ``dlopen`` failure, by-name lookup miss, or a required ABI symbol could not be resolved)." + config_init_error, Counter, "Total configurations that failed to initialize after the module loaded successfully (the module rejected or failed to parse the supplied configuration)." + remote_fetch_error, Counter, "Total failures fetching or loading a remote module source, including rejected cache misses when ``nack_on_cache_miss`` is set. Only the HTTP filter supports remote module sources." + per_route_config_error, Counter, "Total per-route configurations that failed to load or initialize. Only emitted by the HTTP filter." + +In addition to the counters above, a module may define its own custom metrics. These are emitted +under the configurable :ref:`metrics_namespace +` +(``dynamicmodulescustom`` by default), separately from the ``dynamic_modules.`` namespace above. diff --git a/docs/root/intro/arch_overview/http/http_filters.rst b/docs/root/intro/arch_overview/http/http_filters.rst index d7d79d20c7940..0cd6ce91631c7 100644 --- a/docs/root/intro/arch_overview/http/http_filters.rst +++ b/docs/root/intro/arch_overview/http/http_filters.rst @@ -104,7 +104,7 @@ cluster by invoking ``refreshRouteCluster()`` if the cluster specifier of route the :ref:`matcher based cluster specifier ` support the ``refreshRouteCluster()`` callback. -This callabck will not update the cached route but only refresh the target cluster name. This is +This callback will not update the cached route but only refresh the target cluster name. This is suggested to replace ``clearRouteCache()`` if you only want to determine the target cluster based on the latest request attributes that have been updated by the filters and do not want to configure multiple similar routes at the route table. @@ -114,17 +114,17 @@ Security Considerations .. attention:: - **Route Cache Clearing and Authorization Bypass Risk**: When using per-route authorization filters - (such as :ref:`ExtAuthZ `, :ref:`RBAC `, - or :ref:`JWT `), be aware that subsequent filters in the filter - chain may clear the route cache, potentially leading to privilege escalation vulnerabilities. + **Route cache clearing and route-dependent authorization**: Clearing the route cache can cause + Envoy to recompute route matching after earlier HTTP filters have already processed the request. + This can be security-sensitive when filters that make route-dependent authorization decisions + run before filters that mutate route-matching inputs. - **The Problem**: If a request initially matches **Route A** with certain authorization settings, - gets authorized, but then a subsequent filter clears the route cache causing the request - to match **Route B** with different authorization requirements, the request will bypass Route B's - authorization since the authorization filter has already executed. + Route matching may depend on request headers, dynamic metadata, filter state, the request path, + or other request attributes. If one of these inputs is modified by a later filter and that filter + clears the route cache, subsequent filters and the router may observe a different route than the + one seen by earlier filters. - **Filters That Can Clear Route Cache**: + Filters that may clear the route cache include: * :ref:`Lua filter ` - via ``clearRouteCache()`` method * :ref:`ext_proc filter ` - when configured with ``CLEAR`` route cache action or when response contains ``clear_route_cache`` directive @@ -134,12 +134,11 @@ Security Considerations * :ref:`IP tagging filter ` - when sanitizing headers * Custom filters that call ``clearRouteCache()`` on the decoder callbacks - **Mitigation Strategies**: - - * Carefully review the order of filters in your HTTP filter chain when using per-route authorization filters. - * Avoid placing filters that clear route cache after authorization filters unless absolutely necessary. - * Consider using global authorization configuration at the HTTP connection manager level instead of per-route configs whenever possible. - * If route cache clearing is required after authorization, consider re-running authorization checks or using alternative authorization mechanisms. + Operators should carefully review HTTP filter ordering when using route-dependent authorization + filters such as :ref:`RBAC `, :ref:`ExtAuthZ `, + or :ref:`JWT `. Avoid enabling route cache clearing for untrusted + mutation sources, and consider placing route-dependent authorization filters after filters that + mutate route-matching inputs when possible. .. _arch_overview_http_filters_per_filter_config: diff --git a/docs/root/intro/arch_overview/other_features/bandwidth_limiting.rst b/docs/root/intro/arch_overview/other_features/bandwidth_limiting.rst index 146ce50cc8630..ee822256cc2c5 100644 --- a/docs/root/intro/arch_overview/other_features/bandwidth_limiting.rst +++ b/docs/root/intro/arch_overview/other_features/bandwidth_limiting.rst @@ -3,7 +3,12 @@ Bandwidth limiting =================== -Envoy supports local (non-distributed) bandwidth limiting of HTTP requests and response via the +Envoy supports local (non-distributed) bandwidth limiting of HTTP requests and responses via the :ref:`HTTP bandwidth limit filter `. This can be activated globally at the listener level or at a more specific level (e.g.: the virtual host or route level). +Envoy supports local (non-distributed) bandwidth fair-sharing of HTTP requests and responses, via the +:ref:`HTTP bandwidth share filter `. This can be activated +globally at the listener level or at a more specific level (e.g.: the virtual host or route level). +Bandwidth fair sharing is distinct from bandwidth limiting in that one limit can be weighted split +between tenants when overloaded. diff --git a/docs/root/intro/arch_overview/other_protocols/redis.rst b/docs/root/intro/arch_overview/other_protocols/redis.rst index 67fdf0ce86570..0ff4eadcc9ab7 100644 --- a/docs/root/intro/arch_overview/other_protocols/redis.rst +++ b/docs/root/intro/arch_overview/other_protocols/redis.rst @@ -213,6 +213,9 @@ For details on each command's usage see the official GEORADIUSBYMEMBER, Geospatial HDEL, Hash HEXISTS, Hash + HEXPIRE, Hash + HEXPIREAT, Hash + HEXPIRETIME, Hash HGET, Hash HGETALL, Hash HINCRBY, Hash @@ -221,12 +224,18 @@ For details on each command's usage see the official HLEN, Hash HMGET, Hash HMSET, Hash + HPERSIST, Hash + HPEXPIRE, Hash + HPEXPIREAT, Hash + HPEXPIRETIME, Hash + HPTTL, Hash + HRANDFIELD, Hash HSCAN, Hash HSET, Hash HSETNX, Hash HSTRLEN, Hash + HTTL, Hash HVALS, Hash - HRANDFIELD, Hash PFADD, HyperLogLog PFCOUNT, HyperLogLog PFMERGE, HyperLogLog diff --git a/docs/root/intro/arch_overview/security/google_vrp.rst b/docs/root/intro/arch_overview/security/google_vrp.rst index b6c717b219034..c681b1fb993b5 100644 --- a/docs/root/intro/arch_overview/security/google_vrp.rst +++ b/docs/root/intro/arch_overview/security/google_vrp.rst @@ -1,10 +1,10 @@ .. _arch_overview_google_vrp: -Google Vulnerability Reward Program (VRP) +Google Patch Reward Program (PRP) ========================================= -Envoy is a participant in `Google's Vulnerability Reward Program (VRP) -`_. This is open to all security +Envoy is a participant in `Google's Patch Reward Program (PRP) +`_. This is open to all security researchers and will provide rewards for vulnerabilities discovered and reported according to the rules below. @@ -13,7 +13,7 @@ rules below. Rules ----- -The goal of the VRP is to provide a formal process to honor contributions from external +The goal of the PRP is to provide a formal process to honor contributions from external security researchers to Envoy's security. Vulnerabilities should meet the following conditions to be eligible for the program: @@ -22,19 +22,26 @@ to be eligible for the program: :ref:`execution environment ` and be consistent with the program's :ref:`threat model `. -2. Vulnerabilities must be reported to **both** envoy-security@googlegroups.com and via https://bughunters.google.com/report. - Vulenrabilities must be kept under embargo while triage and potential security releases occur. +2. Vulnerabilities must be reported to the Envoy project, preferably by + `opening a GitHub Security Advisory `_ + — alternatively, you may email envoy-security@googlegroups.com + Vulnerabilities must be kept under embargo while triage and potential security releases occur. Please follow the :repo:`disclosure guidance ` when submitting reports. Disclosure SLOs are documented :repo:`here `. In general, security disclosures are subject to the `Linux Foundation's privacy policy - `_ with the added proviso that VRP reports (including - reporter e-mail address and name) may be freely shared with Google for VRP purposes. + `_ with the added proviso that PRP reports (including + reporter e-mail address and name) may be freely shared with Google for PRP purposes. -3. Vulnerabilities must not be previously known in a public forum, e.g. GitHub issues trackers, +3. After vulnerability has been confirmed by the Envoy project and the patch had been publicly released, + you may apply to `Google's Patch Reward program `_. + The program requires you to submit the PR as well as the evidence of improvement, such as resolved security advisory, + which are only available after public disclosure. + +4. Vulnerabilities must not be previously known in a public forum, e.g. GitHub issues trackers, CVE databases (when previously associated with Envoy), etc. Existing CVEs that have not been previously associated with an Envoy vulnerability are fair game. -4. Vulnerabilities must not be also submitted to a parallel reward program run by Google or +5. Vulnerabilities must not be also submitted to a parallel reward program run by Google or `Lyft `_. Rewards are at the discretion of the Envoy OSS security team and Google. They will be conditioned on @@ -42,7 +49,7 @@ the above criteria. If multiple instances of the same vulnerability are reported independent researchers or the vulnerability is already tracked under embargo by the OSS Envoy security team, we will aim to fairly divide the reward amongst reporters. -Rewards should be claimed from Google VRP following the corresponding Envoy security release. +Rewards should be claimed from Google PRP following the corresponding Envoy security release. .. _arch_overview_google_vrp_threat_model: diff --git a/docs/root/intro/arch_overview/security/threat_model.rst b/docs/root/intro/arch_overview/security/threat_model.rst index 1636386d9fc41..f240065ed3de2 100644 --- a/docs/root/intro/arch_overview/security/threat_model.rst +++ b/docs/root/intro/arch_overview/security/threat_model.rst @@ -52,6 +52,24 @@ lack of safe defaults. Over time, we will work towards improved safe-by-default due to backwards compatibility and performance concerns, this will require following the breaking change deprecation policy. +Build configurations +-------------------- + +Issues that do not affect Envoy in published release configurations are not covered by the threat model +and will not be considered security issues. This includes: + +* Debug assertion failures, which are compiled out of Envoy release builds +* Use of TLS libraries other than BoringSSL + +Envoy configurations +-------------------- + +Issues that can only be hit by configuring a feature marked as deprecated (via protobuf annotations) are not +covered by the security policy. + +Issues that can only be hit by enabling a :ref:`runtime flag ` which defaults to +false are not covered by the security policy. + Data and control plane ---------------------- diff --git a/docs/root/intro/arch_overview/upstream/circuit_breaking.rst b/docs/root/intro/arch_overview/upstream/circuit_breaking.rst index 9096c5641ff82..6b347bf76260a 100644 --- a/docs/root/intro/arch_overview/upstream/circuit_breaking.rst +++ b/docs/root/intro/arch_overview/upstream/circuit_breaking.rst @@ -33,8 +33,13 @@ configure and code each application independently. Envoy supports various types :ref:`upstream_rq_pending_overflow ` counter for the cluster will increment. For HTTP/3 the equivalent to HTTP/2's :ref:`max concurrent streams ` is :ref:`max concurrent streams ` * **Cluster maximum requests**: The maximum number of requests that can be outstanding to all hosts - in a cluster at any given time. If this circuit breaker overflows the :ref:`upstream_rq_pending_overflow ` - counter for the cluster will increment. + in a cluster at any given time. If this circuit breaker overflows the + :ref:`upstream_rq_active_overflow ` counter for the cluster + will increment. By default, the legacy :ref:`upstream_rq_pending_overflow ` + counter is no longer incremented for this path; set the runtime flag + ``envoy.reloadable_features.skip_pending_overflow_count_on_active_rq`` to ``false`` to also + increment ``upstream_rq_pending_overflow`` on this path, preserving backwards compatibility + with existing dashboards and alerts. * **Cluster maximum active retries**: The maximum number of retries that can be outstanding to all hosts in a cluster at any given time. In general we recommend using :ref:`retry budgets `; however, if static circuit breaking is preferred it should aggressively circuit break retries. This is so that retries for sporadic failures are allowed, but the overall retry volume cannot diff --git a/docs/root/intro/arch_overview/upstream/load_balancing/load_aware_locality.rst b/docs/root/intro/arch_overview/upstream/load_balancing/load_aware_locality.rst new file mode 100644 index 0000000000000..055658a40f421 --- /dev/null +++ b/docs/root/intro/arch_overview/upstream/load_balancing/load_aware_locality.rst @@ -0,0 +1,487 @@ +:orphan: + +.. _arch_overview_load_balancing_load_aware_locality: + +Load-aware locality load balancing +----------------------------------- + +.. attention:: + + This extension is **work-in-progress** and is not yet implemented. + +The load-aware locality LB policy +(:ref:`envoy.load_balancing_policies.load_aware_locality +`) +is a locality-picking load balancer designed for deployments where incoming +load is not evenly distributed across zones, causing some localities to run +hotter than others. It uses per-endpoint utilization from +`ORCA `_ +reports to weight each locality by its available headroom, preferring the +local zone when load is balanced and spilling to remote zones as the local +zone heats up. + +.. _load_aware_locality_comparison: + +Choosing this policy +^^^^^^^^^^^^^^^^^^^^ + +Use this policy when upstream endpoints report ORCA utilization and Envoy +should make cross-zone routing decisions from observed backend load. It is +most useful when traffic should stay local while zones are similarly loaded, +then spill toward remote localities with more available headroom as the +local zone's load rises. + +Envoy offers three locality-selection strategies. The right choice depends +on whether ORCA reporting is available, whether the control plane supplies +locality weights, and whether the deployment must react to runtime load +imbalance. + +- **Pick this policy when** upstream endpoints emit ORCA utilization and + routing should react to runtime load imbalance from the data plane, + with no control-plane involvement in locality weighting. +- **Pick zone-aware routing when** only local-zone preference is needed + and traffic is already balanced by other means. It has no ORCA + dependency and is simpler to operate, but it only applies at priority + 0 and does not react to backend load. +- **Pick** + :ref:`WrrLocality + ` + **when** the control plane owns locality weights and should compute + them centrally via EDS. Weights are static between updates and do not + react to runtime load. +- **For deterministic routing** (session affinity, consistent hashing), + use ring hash or Maglev. They can also be configured as the + endpoint-picking child policy of this policy, but see + :ref:`Caveats ` for the resulting behavior. + +Architecture +^^^^^^^^^^^^ + +The policy operates at two levels: locality picking (this policy, by +ORCA-derived headroom) and endpoint picking (a configurable child policy). +The split lets you pair load-aware locality selection with whatever +endpoint-picking strategy fits your workload. + +Request path: + +:: + + Incoming request + | + +-- 1. Priority selection (standard healthy/degraded priority load) + | + +-- 2. Locality selection (this policy: weighted random by ORCA headroom) + | + +-- 3. Endpoint selection (child LB) + | + v + Chosen upstream host + +Implementation model +"""""""""""""""""""" + +The policy is implemented as a ``ThreadAwareLoadBalancer``: + +- A main-thread timer recomputes per-locality weights from ORCA data and + publishes an immutable snapshot to worker threads via a thread-local slot. + The snapshot carries a generation counter; workers rebuild per-locality + child LB instances when membership changes bump the generation. +- Worker threads read the latest snapshot lock-free on the request path, + pick a locality, and delegate endpoint selection to the child LB for that + locality. +- ORCA reports flow through per-host ``HostLbPolicyData`` slots shared + with other ORCA consumers; see :ref:`ORCA data flow + ` below for coexistence details. +- When ``enable_oob_load_report`` is set, a cluster-level OOB manager runs + on the main-thread dispatcher and owns one ORCA gRPC streaming session + per host, reacting to membership updates to add and remove sessions. It + decodes reports into the same shared report handler that backs the + in-band path, so workers see OOB and in-band samples through the same + ``HostLbPolicyData`` slots. + +.. _load_aware_locality_orca_data_flow: + +ORCA data flow +"""""""""""""" + +Upstream endpoints must report ORCA utilization. The policy supports both +in-band and out-of-band reporting modes: + +- **In-band (default).** ORCA reports returned on the response headers or + trailers of upstream responses. Sample rate is tied to the request rate + to each host, so probing (``remote_probe_fraction``) is required to keep + remote-locality data fresh. +- **Out-of-band (OOB).** When ``enable_oob_load_report`` is set, the policy + opens a per-host ORCA gRPC stream and the endpoint pushes reports every + ``oob_reporting_period`` independent of request traffic. OOB reuses the + same central ORCA client as + :ref:`CSWRR `: + a cluster-level OOB manager owns one streaming session per host, reacts to + membership changes to add and remove sessions, and feeds every decoded + report into the shared ORCA report handler. Because OOB decouples sample + rate from request rate, ``remote_probe_fraction`` may safely be set to 0. + +Either way reports land in the same per-host ``HostLbPolicyData`` slots, so +weight computation is identical regardless of reporting mode. + +Pairing this policy with +:ref:`CSWRR ` +as ``endpoint_picking_policy`` yields two-level ORCA-aware balancing: +locality selection by aggregate headroom, endpoint selection by +per-endpoint capacity. Each consumer attaches independent +``HostLbPolicyData`` entries, so the two policies do not interfere. + +Utilization is derived from each host's ORCA report using the same +extraction as CSWRR (precedence may be flipped by the +``envoy.reloadable_features.orca_weight_manager_use_named_metrics_first`` +runtime feature). By default: + +1. ``application_utilization`` -- value in [0, 1], used when reported and + greater than 0. +2. Named metrics via ``metric_names_for_computing_utilization`` -- max of + present values, used when ``application_utilization`` is not reported. +3. ``cpu_utilization`` -- final fallback. + +.. _load_aware_locality_weight_computation: + +Weight computation +^^^^^^^^^^^^^^^^^^ + +On each ``weight_update_period`` tick, the main thread recomputes per- +locality routing weights in five stages: + +1. **Filter and average.** Drop hosts whose last ORCA report is older than + ``weight_expiration_period`` and average utilization across the remaining + hosts in each locality. The locality's EWMA state continues unchanged + over the remaining reporters -- there is no synthetic reset. If every + host in a locality is stale, the locality is marked stale and falls + back to host-count weighting in stage 3. +2. **Smooth.** Apply EWMA smoothing per locality. The first sample for a + locality is applied raw (no blending) so the policy begins differentiating + within a single tick after cold start; subsequent samples blend with the + prior smoothed value. At startup with no ORCA data every locality + defaults to utilization 0 (full headroom), so weights reduce to host + counts -- equivalent to round-robin locality selection until the first + reports arrive. +3. **Headroom weight.** Compute each locality's base weight as + ``host_count * (1 - smoothed_util)`` -- capacity-weighted headroom. Stale + localities fall back to ``host_count`` so traffic keeps flowing without + artificially boosting them. +4. **Local preference.** If the local locality's smoothed utilization is at + most ``utilization_variance_threshold`` above the host-count-weighted + remote average, snap to all-local routing. One-sided: if the local + locality is less loaded than the remote localities, all-local routing + always applies regardless of gap size. +5. **Probe floor.** Enforce ``remote_probe_fraction`` by taking a slice of + local weight and redistributing it across remote localities in proportion + to host count -- not headroom -- so all remotes are sampled fairly. The + amount taken from local is capped at the local weight itself. + +Worked example +"""""""""""""" + +Three localities, default variance threshold 0.1: + +- **A** (local): 10 hosts, utilization 0.7 +- **B** (remote): 10 hosts, utilization 0.3 +- **C** (remote): 10 hosts, utilization 0.4 + +Host-count-weighted remote average: ``(0.3*10 + 0.4*10) / 20 = 0.35``. +Local (0.7) exceeds ``0.35 + 0.1 = 0.45``, so spillover is active. + +Headroom weights: A=3, B=7, C=6, total=16. Traffic split: **A ~19%, +B ~44%, C ~37%** -- traffic flows from the hot local zone toward +localities with more headroom. + +If load rebalances and all localities converge to ~0.45, local is within +threshold and the policy snaps to **100% local** (minus the 3% remote +probe). Asymmetric host counts shift the weighted average accordingly: a +larger remote locality pulls the average toward its own utilization. + +Pseudocode +"""""""""" + +The pseudocode below specifies the exact semantics of the five stages +above: + +:: + + # Per-tick smoothing factor (consistent settling regardless of tick rate) + alpha = 1 - exp(-weight_update_period / smoothing_time_constant) + + # Per-host sample validity filter (excludes hosts whose last ORCA report + # is older than weight_expiration_period; if disabled, all hosts qualify) + valid(h) = (now - last_report_time(h)) <= weight_expiration_period + valid_hosts(L) = { h in hosts(L) : valid(h) } + + # Per-locality utilization (EWMA smoothed; first sample applied raw so + # the policy reacts within one tick instead of waiting ~5 time constants + # to converge from the cold-start prior of 0) + if valid_hosts(L) is empty: + smoothed_util(L) = prev_smoothed_util(L) # carry prior value + stale(L) = true + else: + raw_util(L) = avg over h in valid_hosts(L) of util(h) + if no prior smoothed_util(L): # first sample for L + smoothed_util(L) = raw_util(L) + else: + smoothed_util(L) = alpha * raw_util(L) + + (1 - alpha) * prev_smoothed_util(L) + stale(L) = false + + # Base headroom weight; stale localities use host_count baseline + if stale(L): + base_weight(L) = host_count(L) + else: + headroom(L) = max(0, 1 - smoothed_util(L)) + base_weight(L) = host_count(L) * headroom(L) + + total_base_weight = sum(base_weight(L_i) for all L_i) + remote_host_count = sum(host_count(R_i) for all remote R_i) + + # All-overloaded fallback + if total_base_weight == 0: + adjusted_weight(L_i) = host_count(L_i) + else: + adjusted_weight(L_i) = base_weight(L_i) + + if local exists and remote_host_count > 0: + # Local preference (one-sided: local must not be too far ABOVE remote) + remote_weighted_avg = sum(smoothed_util(R_i) * host_count(R_i)) + / remote_host_count + if smoothed_util(local) <= remote_weighted_avg + utilization_variance_threshold: + adjusted_weight(local) = total_base_weight + adjusted_weight(R_i) = 0 + + + # Remote probe enforcement. Conserve total weight: take only as + # much from local as it actually has, and redistribute exactly + # that amount across remotes. + total_adjusted_weight = sum(adjusted_weight(L_i) for all L_i) + remote_weight = sum(adjusted_weight(R_i) for all remote R_i) + remote_share = remote_weight / total_adjusted_weight + if remote_share < remote_probe_fraction: + deficit = remote_probe_fraction * total_adjusted_weight - remote_weight + take_from_local = min(deficit, adjusted_weight(local)) + adjusted_weight(local) -= take_from_local + for each remote R_i: + adjusted_weight(R_i) += take_from_local * host_count(R_i) / remote_host_count + + routing_share(L) = adjusted_weight(L) / sum(adjusted_weight(L_i) for all L_i) + +Host-count proportional probe redistribution is intentional: when probing +for fresh data, the policy samples remote localities fairly rather than +biasing toward localities whose current (possibly stale) utilization +happens to look lower. + +Example configuration +^^^^^^^^^^^^^^^^^^^^^ + +Minimal configuration with round robin endpoint picking: + +.. code-block:: yaml + + load_balancing_policy: + policies: + - typed_extension_config: + name: envoy.load_balancing_policies.load_aware_locality + typed_config: + "@type": type.googleapis.com/envoy.extensions.load_balancing_policies.load_aware_locality.v3.LoadAwareLocality + endpoint_picking_policy: + policies: + - typed_extension_config: + name: envoy.load_balancing_policies.round_robin + typed_config: + "@type": type.googleapis.com/envoy.extensions.load_balancing_policies.round_robin.v3.RoundRobin + +Configuration parameters +^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 35 10 55 + + * - Parameter + - Default + - Description + * - ``endpoint_picking_policy`` + - (required) + - Child LB policy for selecting an endpoint within the chosen locality. + Any LB policy may be configured here, including ring hash and Maglev, + though policies that build cluster-wide structures will operate over + only the chosen locality's host slice. See + :ref:`Caveats `. + * - ``weight_update_period`` + - 1 s + - How often locality weights are recomputed from ORCA data. Must be at + least 100 ms. + * - ``metric_names_for_computing_utilization`` + - (unset) + - Named ORCA metrics used to compute utilization when + ``application_utilization`` is not reported. The max of matching + values is taken. Map entries use ``.`` (e.g. + ``named_metrics.foo``). See + :ref:`Weight computation ` for + precedence. + * - ``utilization_variance_threshold`` + - 0.1 + - When the local locality's utilization exceeds the host-count-weighted + remote average by no more than this threshold, all traffic routes + locally. One-sided check: if the local locality is less loaded than + the remote localities, all-local routing always applies. Range: + [0, 1]. + * - ``smoothing_time_constant`` + - 5 s + - EWMA time constant for per-locality utilization smoothing. The + per-tick smoothing factor is derived as + ``alpha = 1 - exp(-weight_update_period / smoothing_time_constant)``, + so settling time is independent of the configured tick rate. Larger + values produce more stable weights; smaller values react faster. + Must be greater than 0 s. + * - ``remote_probe_fraction`` + - 0.03 + - Minimum fraction of traffic sent to non-local localities to keep ORCA + data fresh in all-local mode. The deficit is redistributed + proportionally to host count. Set to 0 to disable (safe only with + out-of-band ORCA reporting or when cross-zone traffic must be strictly + avoided). Range: [0, 1). See + :ref:`Caveats ` for scaling notes. + * - ``weight_expiration_period`` + - 3 minutes + - Per-host sample validity window. Hosts that have not reported within + this duration are excluded from their locality's utilization + aggregation. The locality's EWMA continues over the remaining + reporting hosts; if every host in a locality is stale, the locality + falls back to host-count-proportional weighting. Tune higher to + tolerate longer reporting gaps; tune lower to prune draining + backends faster. Set to 0 s to disable expiration. + * - ``enable_oob_load_report`` + - (unset / false) + - Enables out-of-band (OOB) ORCA utilization reporting. When set, the + policy opens a per-host ORCA gRPC stream and the endpoint pushes + reports on its own schedule rather than piggybacking on responses. + When unset, only in-band reports on response headers/trailers are + consumed. See :ref:`ORCA data flow + `. + * - ``oob_reporting_period`` + - 10 s + - Requested load-reporting interval, used only when + ``enable_oob_load_report`` is true. The upstream may report less + frequently than requested. + +Priority support +^^^^^^^^^^^^^^^^ + +The policy respects Envoy's +:ref:`priority levels `. +Priority selection happens first via the standard healthy/degraded +priority load calculation; locality selection then applies within the +chosen priority. Unlike zone-aware routing (priority 0 only), this +policy applies at all priority levels. + +Three independent weight sets are maintained per priority: + +- **Healthy** -- common case, healthy hosts only. +- **Degraded** -- when Envoy selects + :ref:`degraded ` hosts. +- **All-host** -- when the priority is in + :ref:`panic mode `. + +Each set tracks its own per-locality utilization average and headroom +weight, computed from the same per-host ORCA data in a single tick pass. + +.. _load_aware_locality_caveats: + +Caveats and known limitations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **Probing is required with in-band reporting.** In the default in-band + mode, a locality only produces fresh ORCA samples when it receives + traffic, so ``remote_probe_fraction`` must stay above 0 to keep remote + localities reporting. Set it to 0 only when ``enable_oob_load_report`` + is on (OOB streams report independently of traffic) or when cross-zone + traffic must be strictly avoided. +- **Hash-based child policies.** Ring hash and Maglev build their hash + structures from the host set they are given. With this policy, that set + is the chosen locality's hosts, not the full cluster. The same request + hash will not necessarily map to the same endpoint cluster-wide, so + consistency guarantees apply only within a locality. +- **Probe-fraction scaling.** ``remote_probe_fraction`` is a global value + divided across all remote localities, then again across each locality's + hosts. The per-host probe rate is therefore approximately + ``Total RPS * remote_probe_fraction / (N remotes * Hosts/locality)``, + and the expected interval between consecutive probes to a given host + is the reciprocal. When that interval exceeds + ``weight_expiration_period``, hosts are likely to go stale between + probes and the locality falls back to host-count weighting -- defeating + the load-awareness this policy provides. + + Approximate sample intervals per host at the default ``remote_probe_fraction`` + of 0.03: + + +-----------+-----------+----------------+----------------------+ + | Total RPS | N remotes | Hosts/locality | Sample interval/host | + +===========+===========+================+======================+ + | 1000 | 3 | 10 | ~1 s | + +-----------+-----------+----------------+----------------------+ + | 1000 | 100 | 10 | ~33 s | + +-----------+-----------+----------------+----------------------+ + | 100 | 100 | 10 | ~5.5 min | + +-----------+-----------+----------------+----------------------+ + + The top row is comfortably faster than the 3-minute default expiration. + The middle row is still safe but leaves less headroom. In the bottom + row, samples expire before the next probe arrives, so remote + localities will alternate between fresh data and host-count fallback + every few ticks. To avoid this, either reduce locality count, raise + ``remote_probe_fraction``, raise ``weight_expiration_period`` to + tolerate longer gaps, or enable OOB ORCA reporting via + ``enable_oob_load_report`` (which decouples sample rate from request + rate entirely). +- **Variance-threshold oscillation.** Workloads sitting near the + ``utilization_variance_threshold`` boundary can theoretically oscillate + between snap-to-local and spillover modes across consecutive ticks. + EWMA smoothing dampens this in practice; tune + ``smoothing_time_constant`` higher if oscillation is observed. +- **Subsetting.** Load balancer + :ref:`subsetting ` partitions + hosts orthogonally to locality boundaries. The policy will operate + over the post-subset host slice; per-locality weights are computed + over whatever hosts remain after subsetting filters them. This is + rarely the behavior subset users expect. + +Statistics +^^^^^^^^^^ + +The policy emits stats under ``cluster..load_aware_locality.*``: + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Counter + - Increments when + * - ``recompute_total`` + - Per main-thread tick that recomputes weights. + * - ``all_overloaded_total`` + - Per tick where every locality's headroom was 0 (fallback to + host-count weighting). + * - ``local_preferred_total`` + - Per tick where the variance-threshold check snapped routing to + 100% local. + * - ``probe_active_total`` + - Per tick where ``remote_probe_fraction`` redistribution kicked in. + * - ``stale_locality_total`` + - Incremented once per stale locality per tick (a locality whose + hosts were all stale and fell back to host-count baseline). A + 5-locality cluster with 2 stale localities adds 2 each tick. + +Migrating from zone-aware routing? The closest counter mappings: + ++--------------------------------------+----------------------------------------------+ +| Zone-aware counter | Load-aware-locality equivalent | ++======================================+==============================================+ +| ``lb_zone_routing_all_directly`` | ``load_aware_locality.local_preferred_total``| ++--------------------------------------+----------------------------------------------+ +| ``lb_recalculate_zone_structures`` | ``load_aware_locality.recompute_total`` | ++--------------------------------------+----------------------------------------------+ diff --git a/docs/root/operations/admin.rst b/docs/root/operations/admin.rst index 536cb40621dae..e429cd6c1eb72 100644 --- a/docs/root/operations/admin.rst +++ b/docs/root/operations/admin.rst @@ -260,6 +260,14 @@ modify different aspects of the server: Dump current heap profile of Envoy process. The output content is parsable binary by the ``pprof`` tool. Requires compiling with tcmalloc (default). +.. _operations_admin_interface_peak_heap_dump: + +.. http:get:: /peak_heap_dump + + Dump peak heap profile of Envoy process. This captures the heap state at peak memory usage. + The output content is parsable binary by the ``pprof`` tool. + Requires compiling with tcmalloc (default). + .. http:post:: /allocprofiler Enable or disable the allocation profiler. The output content is parsable binary by the ``pprof`` tool. @@ -342,6 +350,7 @@ modify different aspects of the server: source/common/network/udp_listener_impl.cc: trace - ``/logging?paths=source/common/event/dispatcher_impl.cc:debug`` will make the level of ``source/common/event/dispatcher_impl.cc`` be debug. + - ``/logging?group=http:info`` will make the level of all loggers in the ``http`` group be info. - ``/logging?admin_filter=info`` will make the level of ``source/server/admin/admin_filter.cc`` be info, and other unmatched loggers will be the default trace. - ``/logging?paths=source/common*:warning`` will make the level of ``source/common/event/dispatcher_impl.cc:``, ``source/common/network/tcp_listener_impl.cc`` be warning. Other unmatched loggers will be the default trace, e.g., `admin_filter.cc`, even it was updated to info from the previous post update. diff --git a/docs/root/operations/cli.rst b/docs/root/operations/cli.rst index 7ecbf9fc48bbc..aefe58eb67c0f 100644 --- a/docs/root/operations/cli.rst +++ b/docs/root/operations/cli.rst @@ -138,6 +138,7 @@ following are the command line options that Envoy supports. :%v: The actual message to log ("some user text") :%_: The actual message to log, but with escaped newlines (from (if using ``%v``) "some user text\nbelow", to "some user text\\nbelow") :%j: The actual message to log as JSON escaped string (https://tools.ietf.org/html/rfc7159#page-8). + :%N: The Envoy version string: ``{revision}/{version}/{status}/{build_type}/{ssl_version}`` (e.g. "c93f9f6c1e5adddd10a3e3646c7e049c649ae177/1.38.0/Clean/RELEASE/BoringSSL"), matching the output of :option:`--version`. :%t: Thread id ("1232") :%P: Process id ("3456") :%n: Logger's name ("filter") diff --git a/docs/root/operations/traffic_tapping.rst b/docs/root/operations/traffic_tapping.rst index 098bab5599b7e..c70541738e26c 100644 --- a/docs/root/operations/traffic_tapping.rst +++ b/docs/root/operations/traffic_tapping.rst @@ -80,6 +80,16 @@ emitted. When streaming, a series of :ref:`SocketStreamedTraceSegment See the :ref:`HTTP tap filter streaming ` documentation for more information. Most of the concepts overlap between the HTTP filter and the transport socket. +Sampling +-------- + +Configure :ref:`tap_enabled ` to sample a +fraction of connections. Only the configured fraction of connections proceeds to match-predicate +evaluation; the remainder is not tapped and increments the ``cx_sampled_out`` statistic. The +configured sampling rate is recorded on :ref:`configured_sample_rate +` of the first emitted +``TraceWrapper`` segment of each trace, as with the HTTP filter. + Statistics ---------- @@ -91,8 +101,9 @@ To customize the prefix used in these statistics, configure the :ref:`stats_pref :header: Name, Type, Description :widths: 1, 1, 2 - streamed_submit, Counter, The total count of submissions triggered by streamed trace events buffered_submit, Counter, The total count of submissions triggered by buffered trace events + cx_sampled_out, Counter, Total connections short-circuited by ``tap_enabled`` sampling before match evaluation + streamed_submit, Counter, The total count of submissions triggered by streamed trace events PCAP generation --------------- diff --git a/docs/root/start/install.rst b/docs/root/start/install.rst index c6c3205c7f664..5f246b66142e8 100644 --- a/docs/root/start/install.rst +++ b/docs/root/start/install.rst @@ -13,57 +13,14 @@ getting your Envoy proxy up and running. Install Envoy on Debian-based Linux ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - If you are using a different deb-based distribution to the ones shown below, you may still be able to use one of them. - -.. tabs:: - - .. code-tab:: console Debian bookworm - - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io bookworm main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version - - .. code-tab:: console Debian bullseye - - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io bullseye main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version - - .. code-tab:: console Debian trixie - - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io trixie main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version - - .. code-tab:: console Ubuntu focal - - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io focal main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version - - .. code-tab:: console Ubuntu jammy - - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io jammy main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version - - .. code-tab:: console Ubuntu noble +.. note:: - $ wget -O- https://apt.envoyproxy.io/signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/envoy-keyring.gpg - $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/envoy-keyring.gpg] https://apt.envoyproxy.io noble main" | sudo tee /etc/apt/sources.list.d/envoy.list - $ sudo apt-get update - $ sudo apt-get install envoy - $ envoy --version + The apt repository at ``https://apt.envoyproxy.io`` has not been updated for + some time and is not currently maintained. Please use the + :ref:`pre-built Docker images ` or download the static + binary from the `GitHub release page `__. + See `issue #44405 `__ for + the tracking discussion. .. _start_install_macosx: diff --git a/docs/tools/protodoc/protodoc.py b/docs/tools/protodoc/protodoc.py index 8d9f0cd46ad5b..3a405ad0438e7 100755 --- a/docs/tools/protodoc/protodoc.py +++ b/docs/tools/protodoc/protodoc.py @@ -11,7 +11,7 @@ from functools import cached_property, lru_cache from typing import Dict, Iterable, Iterator, Set, Tuple -from google.protobuf.descriptor_pb2 import FieldDescriptorProto as field_proto +from google.protobuf.descriptor_pb2 import FieldDescriptorProto as field_proto # noqa: N813 import yaml from udpa.annotations import security_pb2 diff --git a/docs/tools/python/BUILD b/docs/tools/python/BUILD index e69de29bb2d1d..a8583d6748ffa 100644 --- a/docs/tools/python/BUILD +++ b/docs/tools/python/BUILD @@ -0,0 +1,18 @@ +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + +licenses(["notice"]) # Apache 2 + +exports_files([ + "entry_point.py", +]) + +compile_pip_requirements( + name = "requirements", + src = "requirements.in", + extra_args = [ + "--allow-unsafe", + "--generate-hashes", + "--reuse-hashes", + "--resolver=backtracking", + ], +) diff --git a/docs/tools/python/requirements.in b/docs/tools/python/requirements.in index a2f904350b43b..0fab83591c703 100644 --- a/docs/tools/python/requirements.in +++ b/docs/tools/python/requirements.in @@ -1,5 +1,5 @@ # Docs-specific dependencies -envoy.docs.sphinx_runner>=0.2.9 +envoy.docs.sphinx_runner>=0.3.2 sphinx>=8.1.3,<9 sphinxcontrib-applehelp>=1.0.8 sphinxcontrib-devhelp>=1.0.6 @@ -11,10 +11,11 @@ sphinx-rtd-theme>=3.0.2 # Shared dependencies (also in main workspace) aio.run.runner -envoy.base.utils>=0.5.10 -envoy.code.check>=0.5.12 +envoy.base.utils>=0.6.9 +envoy.code.check>=0.6.7 frozendict>=2.3.7 jinja2 packaging +pyjwt[crypto]==2.13.0 +cryptography==49.0.0 pyyaml -uvloop==0.20.0 diff --git a/docs/tools/python/requirements.txt b/docs/tools/python/requirements.txt index 0b39836ffaa30..59ec020ff2998 100644 --- a/docs/tools/python/requirements.txt +++ b/docs/tools/python/requirements.txt @@ -2,35 +2,37 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes requirements.in +# bazel run //tools/python:requirements.update # -abstracts==0.0.12 \ - --hash=sha256:acc01ff56c8a05fb88150dff62e295f9071fc33388c42f1dfc2787a8d1c755ff +abstracts==0.2.0 \ + --hash=sha256:1dd6df509a67cae1722c0dd9f58cdd6dee6e42f45bf0d7197e159b2e4c5e98e9 \ + --hash=sha256:995f326ab4408a6652a346439cb33169f48e66f25515c6baf9fc47f1a3e4652a # via # aio-api-github # aio-core + # aio-run-checker # aio-run-runner # envoy-base-utils # envoy-code-check -aio-api-github==0.2.10 \ - --hash=sha256:2563ad2cd219352a7933b78190c17ab460b503b437bec348a738c0f804adf45f \ - --hash=sha256:75e17073e9e03386d719d1af355f599c16d68345992eaa97aebfa6159b07ad02 +aio-api-github==0.3.1 \ + --hash=sha256:04d2e3fb4fcb55927b75f9a3cefae7673cefabd2387dd2416848ff19326d719c \ + --hash=sha256:d083ca09e5577d0c4217138e45b18ea63d57d0cbb030eb336cc2a979f8b244b4 # via envoy-base-utils -aio-core==0.10.5 \ - --hash=sha256:ba512132df47514a15930b89835ba1ef13522b7e31ec83a3b0622b7431f3f42e \ - --hash=sha256:d8486a0115ade8e0362783b8e7cd988368766d0d911c62cb73b716bfe1f50a92 +aio-core==0.11.1 \ + --hash=sha256:03f1202029655c68d5d41c2fa1d18e344d61515b71080f770f8cf6f99fbc5f5a \ + --hash=sha256:76608a1dad3985866c1b3ffc81e0d08bd1a740e8e72125bfbf46d1ee6fc3abfe # via # aio-api-github # aio-run-runner # envoy-base-utils # envoy-code-check -aio-run-checker==0.5.8 \ - --hash=sha256:9bf5e20773f83113b2eee39277f0ec3e014aeecfd33deb44103b0ed2482d3ff5 \ - --hash=sha256:b5fcb91651da986b13eefd340a45b402100a2967e1eb1a1b941ccfbc36fe0eda +aio-run-checker==0.6.1 \ + --hash=sha256:0c929c3534600b5f163de177892841ed36267472a8d0609f550d717a523f28fd \ + --hash=sha256:17f8ccf7976406adc062c5042888dab9507c5332bb00b697c3bd48e56a1ffee0 # via envoy-code-check -aio-run-runner==0.3.4 \ - --hash=sha256:3f7bdb91c9a7a60d547d18c620ce2444b4e81d6e6a64a2e1ef465e54db8708bf \ - --hash=sha256:ffa5693d6452b9fc07674cbea665825959d3ea5adca4e492998d242dadf3a773 +aio-run-runner==0.4.1 \ + --hash=sha256:03c2978e4c7e1d02a55e25b0c081603d7900387bcce4fe80b8e8af8f3284e05b \ + --hash=sha256:43c80688c50ba3a528d88635fee5653b9f0f40fc4a036e1aeaddf8ada3794d2e # via # -r requirements.in # aio-run-checker @@ -40,127 +42,245 @@ aiohappyeyeballs==2.6.1 \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 # via aiohttp -aiohttp==3.13.5 \ - --hash=sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5 \ - --hash=sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b \ - --hash=sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9 \ - --hash=sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b \ - --hash=sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8 \ - --hash=sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a \ - --hash=sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2 \ - --hash=sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1 \ - --hash=sha256:147b4f501d0292077f29d5268c16bb7c864a1f054d7001c4c1812c0421ea1ed0 \ - --hash=sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037 \ - --hash=sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416 \ - --hash=sha256:178c7b5e62b454c2bc790786e6058c3cc968613b4419251b478c153a4aec32b1 \ - --hash=sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9 \ - --hash=sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a \ - --hash=sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36 \ - --hash=sha256:206b7b3ef96e4ce211754f0cd003feb28b7d81f0ad26b8d077a5d5161436067f \ - --hash=sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b \ - --hash=sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174 \ - --hash=sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b \ - --hash=sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8 \ - --hash=sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e \ - --hash=sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6 \ - --hash=sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c \ - --hash=sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe \ - --hash=sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9 \ - --hash=sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc \ - --hash=sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800 \ - --hash=sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286 \ - --hash=sha256:347542f0ea3f95b2a955ee6656461fa1c776e401ac50ebce055a6c38454a0adf \ - --hash=sha256:39380e12bd1f2fdab4285b6e055ad48efbaed5c836433b142ed4f5b9be71036a \ - --hash=sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc \ - --hash=sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9 \ - --hash=sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665 \ - --hash=sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832 \ - --hash=sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297 \ - --hash=sha256:4e704c52438f66fdd89588346183d898bb42167cf88f8b7ff1c0f9fc957c348f \ - --hash=sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73 \ - --hash=sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b \ - --hash=sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9 \ - --hash=sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090 \ - --hash=sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49 \ - --hash=sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d \ - --hash=sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46 \ - --hash=sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83 \ - --hash=sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796 \ - --hash=sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be \ - --hash=sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d \ - --hash=sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf \ - --hash=sha256:6f497a6876aa4b1a102b04996ce4c1170c7040d83faa9387dd921c16e30d5c83 \ - --hash=sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25 \ - --hash=sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06 \ - --hash=sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3 \ - --hash=sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6 \ - --hash=sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb \ - --hash=sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88 \ - --hash=sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9 \ - --hash=sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be \ - --hash=sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14 \ - --hash=sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a \ - --hash=sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c \ - --hash=sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3 \ - --hash=sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f \ - --hash=sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d \ - --hash=sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670 \ - --hash=sha256:912d4b6af530ddb1338a66229dac3a25ff11d4448be3ec3d6340583995f56031 \ - --hash=sha256:9277145d36a01653863899c665243871434694bcc3431922c3b35c978061bdb8 \ - --hash=sha256:95d14ca7abefde230f7639ec136ade282655431fd5db03c343b19dda72dd1643 \ - --hash=sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d \ - --hash=sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8 \ - --hash=sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8 \ - --hash=sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1 \ - --hash=sha256:9efcc0f11d850cefcafdd9275b9576ad3bfb539bed96807663b32ad99c4d4b88 \ - --hash=sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb \ - --hash=sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61 \ - --hash=sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4 \ - --hash=sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7 \ - --hash=sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9 \ - --hash=sha256:a8a4d3427e8de1312ddf309cc482186466c79895b3a139fed3259fc01dfa9a5b \ - --hash=sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500 \ - --hash=sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6 \ - --hash=sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2 \ - --hash=sha256:af545c2cffdb0967a96b6249e6f5f7b0d92cdfd267f9d5238d5b9ca63e8edb10 \ - --hash=sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1 \ - --hash=sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3 \ - --hash=sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e \ - --hash=sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a \ - --hash=sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5 \ - --hash=sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95 \ - --hash=sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074 \ - --hash=sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5 \ - --hash=sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b \ - --hash=sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772 \ - --hash=sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a \ - --hash=sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274 \ - --hash=sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94 \ - --hash=sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13 \ - --hash=sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac \ - --hash=sha256:cb979826071c0986a5f08333a36104153478ce6018c58cba7f9caddaf63d5d67 \ - --hash=sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76 \ - --hash=sha256:d147004fede1b12f6013a6dbb2a26a986a671a03c6ea740ddc76500e5f1c399f \ - --hash=sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8 \ - --hash=sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7 \ - --hash=sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8 \ - --hash=sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3 \ - --hash=sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be \ - --hash=sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b \ - --hash=sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c \ - --hash=sha256:e999f0c88a458c836d5fb521814e92ed2172c649200336a6df514987c1488258 \ - --hash=sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c \ - --hash=sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6 \ - --hash=sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56 \ - --hash=sha256:ee5e86776273de1795947d17bddd6bb19e0365fd2af4289c0d2c5454b6b1d36b \ - --hash=sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d \ - --hash=sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a \ - --hash=sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162 \ - --hash=sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1 \ - --hash=sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6 \ - --hash=sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5 \ - --hash=sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540 \ - --hash=sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254 +aiohttp==3.14.1 \ + --hash=sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5 \ + --hash=sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5 \ + --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ + --hash=sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983 \ + --hash=sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521 \ + --hash=sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521 \ + --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ + --hash=sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340 \ + --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ + --hash=sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d \ + --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ + --hash=sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a \ + --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ + --hash=sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4 \ + --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ + --hash=sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a \ + --hash=sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f \ + --hash=sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f \ + --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ + --hash=sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee \ + --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ + --hash=sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8 \ + --hash=sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb \ + --hash=sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb \ + --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ + --hash=sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397 \ + --hash=sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05 \ + --hash=sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05 \ + --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ + --hash=sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8 \ + --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ + --hash=sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09 \ + --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ + --hash=sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2 \ + --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ + --hash=sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba \ + --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ + --hash=sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf \ + --hash=sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271 \ + --hash=sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271 \ + --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ + --hash=sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5 \ + --hash=sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847 \ + --hash=sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847 \ + --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ + --hash=sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264 \ + --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ + --hash=sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf \ + --hash=sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6 \ + --hash=sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6 \ + --hash=sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df \ + --hash=sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df \ + --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ + --hash=sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035 \ + --hash=sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126 \ + --hash=sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126 \ + --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ + --hash=sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6 \ + --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ + --hash=sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35 \ + --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ + --hash=sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4 \ + --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ + --hash=sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333 \ + --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ + --hash=sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203 \ + --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ + --hash=sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c \ + --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ + --hash=sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1 \ + --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ + --hash=sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251 \ + --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ + --hash=sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365 \ + --hash=sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b \ + --hash=sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b \ + --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ + --hash=sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621 \ + --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ + --hash=sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94 \ + --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ + --hash=sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da \ + --hash=sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 \ + --hash=sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491 \ + --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ + --hash=sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe \ + --hash=sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d \ + --hash=sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d \ + --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ + --hash=sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080 \ + --hash=sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42 \ + --hash=sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42 \ + --hash=sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c \ + --hash=sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c \ + --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ + --hash=sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397 \ + --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ + --hash=sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9 \ + --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ + --hash=sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8 \ + --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ + --hash=sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345 \ + --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ + --hash=sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3 \ + --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ + --hash=sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602 \ + --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ + --hash=sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2 \ + --hash=sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966 \ + --hash=sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966 \ + --hash=sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192 \ + --hash=sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192 \ + --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ + --hash=sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95 \ + --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ + --hash=sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3 \ + --hash=sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b \ + --hash=sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b \ + --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ + --hash=sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444 \ + --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ + --hash=sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6 \ + --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ + --hash=sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573 \ + --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ + --hash=sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af \ + --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ + --hash=sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15 \ + --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ + --hash=sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe \ + --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ + --hash=sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2 \ + --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ + --hash=sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496 \ + --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ + --hash=sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876 \ + --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ + --hash=sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817 \ + --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ + --hash=sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448 \ + --hash=sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e \ + --hash=sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e \ + --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ + --hash=sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6 \ + --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ + --hash=sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd \ + --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ + --hash=sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f \ + --hash=sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe \ + --hash=sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe \ + --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ + --hash=sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c \ + --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ + --hash=sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca \ + --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ + --hash=sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c \ + --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ + --hash=sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa \ + --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ + --hash=sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc \ + --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ + --hash=sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0 \ + --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ + --hash=sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0 \ + --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ + --hash=sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2 \ + --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ + --hash=sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844 \ + --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ + --hash=sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719 \ + --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ + --hash=sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1 \ + --hash=sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3 \ + --hash=sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3 \ + --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ + --hash=sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178 \ + --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ + --hash=sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3 \ + --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ + --hash=sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95 \ + --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ + --hash=sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730 \ + --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ + --hash=sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842 \ + --hash=sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd \ + --hash=sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd \ + --hash=sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d \ + --hash=sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d \ + --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ + --hash=sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96 \ + --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ + --hash=sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85 \ + --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ + --hash=sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1 \ + --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ + --hash=sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199 \ + --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ + --hash=sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a \ + --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ + --hash=sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588 \ + --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ + --hash=sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec \ + --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ + --hash=sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004 \ + --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ + --hash=sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480 \ + --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ + --hash=sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04 \ + --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ + --hash=sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8 \ + --hash=sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce \ + --hash=sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce \ + --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ + --hash=sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087 \ + --hash=sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505 \ + --hash=sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505 \ + --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ + --hash=sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780 \ + --hash=sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4 \ + --hash=sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4 \ + --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ + --hash=sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d \ + --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ + --hash=sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca \ + --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ + --hash=sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665 \ + --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ + --hash=sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296 \ + --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ + --hash=sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c \ + --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ + --hash=sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a \ + --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ + --hash=sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7 \ + --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ + --hash=sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451 \ + --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 \ + --hash=sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3 # via # aio-api-github # envoy-base-utils @@ -451,25 +571,28 @@ docutils==0.21.2 \ # envoy-docs-sphinx-runner # sphinx # sphinx-rtd-theme -envoy-base-utils==0.5.11 \ - --hash=sha256:008ebe43ecbde736e8033a34242659ff784caf50cc3301b1d3214899e31fa83d \ - --hash=sha256:be942364a42d92439281700486cf5040bdbcf3786a4e1dbcc9ea0af033a7e47e + # sphinx-tabs +envoy-base-utils==0.6.9 \ + --hash=sha256:664dcc19ee12125398222a7628bdcbb40e265b3846e197e4014434fcd273fd61 \ + --hash=sha256:b088b7621b5075005c48cdbef5db5675827b7b63550be7d880d395261fdcdb20 # via # -r requirements.in # envoy-code-check # envoy-docs-sphinx-runner -envoy-code-check==0.5.14 \ - --hash=sha256:56ed152ba633b8d6846509424a4dc94996ed0eb3d656495ec2b32a94144ed904 \ - --hash=sha256:83cce34e3c2ee3d1b9a375922ee0d08fedbd8d3621f72d3ee55dd0a7d26a0e0e - # via -r requirements.in -envoy-docs-sphinx-runner==0.2.12 \ - --hash=sha256:b07e753f4a4694ff9786c943647702acd75c6bf9a7044e166ce4cf6d58a44af2 \ - --hash=sha256:ebe1620f19816edcc2fbf3908617fd15b6492b87e4276a2e76be45f3c7b50f9c +envoy-code-check==0.6.7 \ + --hash=sha256:76b0edf0b95d8a31b5d78481b3a7f5ae4a2925c04d2158e77cc3549653a4213e \ + --hash=sha256:7b7fbc99c114f663126be738095945d479e0b4c6f71d3d784bc234d9d9098a8d + # via -r tools/python/requirements.in +envoy-docs-sphinx-runner==0.3.2 \ + --hash=sha256:76ca59da2237b6aabc453ed067808aa7d603913e1de0ad8b2db3dddee99190be \ + --hash=sha256:fb66b380809bf910e45c904477510c0ccb2c808f65ff7a8794d9c65ab3d3bbae # via -r requirements.in flake8==7.3.0 \ --hash=sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e \ --hash=sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872 - # via envoy-code-check + # via + # envoy-code-check + # pep8-naming frozendict==2.4.7 \ --hash=sha256:05dd27415f913cd11649009f53d97eb565ce7b76787d7869c4733738c10e8d27 \ --hash=sha256:0664092614d2b9d0aa404731f33ad5459a54fe8dab9d1fd45aa714fa6de4d0ef \ @@ -708,9 +831,9 @@ humanfriendly==10.0 \ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc # via coloredlogs -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc # via # requests # yarl @@ -968,6 +1091,7 @@ multidict==6.7.1 \ --hash=sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba \ --hash=sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 # via + # aio-api-github # aiohttp # yarl orjson==3.11.7 \ @@ -1046,9 +1170,9 @@ orjson==3.11.7 \ --hash=sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1 \ --hash=sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867 # via envoy-base-utils -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +packaging==26.2 \ + --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ + --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # -r requirements.in # aio-api-github @@ -1060,6 +1184,10 @@ pathspec==1.0.4 \ --hash=sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645 \ --hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723 # via yamllint +pep8-naming==0.15.1 \ + --hash=sha256:eb63925e7fd9e028c7f7ee7b1e413ec03d1ee5de0e627012102ee0222c273c86 \ + --hash=sha256:f6f4a499aba2deeda93c1f26ccc02f3da32b035c8b2db9696b730ef2c9639d29 + # via envoy-code-check platformdirs==4.5.1 \ --hash=sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda \ --hash=sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31 @@ -1190,17 +1318,15 @@ propcache==0.4.1 \ # via # aiohttp # yarl -protobuf==6.33.5 \ - --hash=sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c \ - --hash=sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02 \ - --hash=sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c \ - --hash=sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd \ - --hash=sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a \ - --hash=sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190 \ - --hash=sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c \ - --hash=sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5 \ - --hash=sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0 \ - --hash=sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b +protobuf==7.35.1 \ + --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ + --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ + --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ + --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ + --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ + --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ + --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ + --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via # envoy-base-utils # envoy-docs-sphinx-runner @@ -1216,28 +1342,23 @@ pyflakes==3.4.0 \ --hash=sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58 \ --hash=sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f # via flake8 -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # envoy-docs-sphinx-runner # sphinx -pyjwt[crypto]==2.12.1 \ - --hash=sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c \ - --hash=sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b + # sphinx-tabs +pyjwt[crypto]==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 # via + # -r requirements.in # gidgethub - # pyjwt python-gnupg==0.5.6 \ --hash=sha256:5743e96212d38923fc19083812dc127907e44dbd3bcf0db4d657e291d3c21eac \ --hash=sha256:b5050a55663d8ab9fcc8d97556d229af337a87a3ebebd7054cbd8b7e2043394a # via envoy-base-utils -pytz==2025.2 \ - --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \ - --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 - # via - # aio-core - # envoy-base-utils pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ @@ -1316,6 +1437,7 @@ pyyaml==6.0.3 \ # -r requirements.in # aio-core # envoy-base-utils + # envoy-docs-sphinx-runner # yamllint requests==2.33.0 \ --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ @@ -1341,6 +1463,7 @@ sphinx==8.2.3 \ # envoy-docs-sphinx-runner # sphinx-copybutton # sphinx-rtd-theme + # sphinx-tabs # sphinxcontrib-googleanalytics # sphinxcontrib-httpdomain # sphinxcontrib-jquery @@ -1355,17 +1478,23 @@ sphinx-rtd-theme==3.1.0 \ # via # -r requirements.in # envoy-docs-sphinx-runner +sphinx-tabs==3.5.0 \ + --hash=sha256:154be49de4d5c8249ea08c5d9bf88ca8f9c31e00a178305a93cbc33e000339e5 \ + --hash=sha256:91dba1187e4c35fd37380a56ac228bbd54c6c649b2351829f3bf033718277537 + # via envoy-docs-sphinx-runner sphinxcontrib-applehelp==2.0.0 \ --hash=sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1 \ --hash=sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5 # via # -r requirements.in + # envoy-docs-sphinx-runner # sphinx sphinxcontrib-devhelp==2.0.0 \ --hash=sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad \ --hash=sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2 # via # -r requirements.in + # envoy-docs-sphinx-runner # sphinx sphinxcontrib-googleanalytics==0.5 \ --hash=sha256:437e529c33c441bcccceabe3ead1585115e6ba83fe90e23bd42e42521333cc0a \ @@ -1376,6 +1505,7 @@ sphinxcontrib-htmlhelp==2.1.0 \ --hash=sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9 # via # -r requirements.in + # envoy-docs-sphinx-runner # sphinx sphinxcontrib-httpdomain==2.0.0 \ --hash=sha256:9e4e8733bf41ee4d9d5f9eb4dbf3cc2c22a665221ba42c5c3ae181b98af8855d \ @@ -1396,6 +1526,7 @@ sphinxcontrib-qthelp==2.0.0 \ --hash=sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb # via # -r requirements.in + # envoy-docs-sphinx-runner # sphinx sphinxcontrib-serializinghtml==2.0.0 \ --hash=sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331 \ @@ -1414,45 +1545,19 @@ trycast==1.3.0 \ # via # aio-core # envoy-base-utils -types-docutils==0.22.3.20251115 \ - --hash=sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16 \ - --hash=sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e - # via types-pygments -types-orjson==3.6.2 \ - --hash=sha256:22ee9a79236b6b0bfb35a0684eded62ad930a88a56797fa3c449b026cf7dbfe4 \ - --hash=sha256:cf9afcc79a86325c7aff251790338109ed6f6b1bab09d2d4262dd18c85a3c638 - # via aio-core -types-protobuf==6.32.1.20251210 \ - --hash=sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140 \ - --hash=sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763 - # via envoy-base-utils -types-pygments==2.19.0.20251121 \ - --hash=sha256:cb3bfde34eb75b984c98fb733ce4f795213bd3378f855c32e75b49318371bb25 \ - --hash=sha256:eef114fde2ef6265365522045eac0f8354978a566852f69e75c531f0553822b1 - # via envoy-docs-sphinx-runner -types-pytz==2025.2.0.20251108 \ - --hash=sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c \ - --hash=sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb - # via - # aio-core - # envoy-base-utils -types-pyyaml==6.0.12.20250915 \ - --hash=sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3 \ - --hash=sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6 - # via - # aio-core - # envoy-code-check typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 - # via aiosignal + # via + # aiohttp + # aiosignal uritemplate==4.2.0 \ --hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \ --hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 # via gidgethub -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests uvloop==0.20.0 \ --hash=sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847 \ @@ -1486,9 +1591,7 @@ uvloop==0.20.0 \ --hash=sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66 \ --hash=sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba \ --hash=sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf - # via - # -r requirements.in - # aio-run-runner + # via aio-run-runner verboselogs==1.7 \ --hash=sha256:d63f23bf568295b95d3530c6864a0b580cec70e7ff974177dead1e4ffbc6ff49 \ --hash=sha256:e33ddedcdfdafcb3a174701150430b11b46ceb64c2a9a26198c76a156568e427 @@ -1632,7 +1735,9 @@ yarl==1.22.0 \ --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 - # via aiohttp + # via + # aio-api-github + # aiohttp zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ --hash=sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a \ diff --git a/docs/versions.yaml b/docs/versions.yaml index 7d4f21e62992b..2f9793ec9168d 100644 --- a/docs/versions.yaml +++ b/docs/versions.yaml @@ -28,6 +28,7 @@ "1.32": 1.32.13 "1.33": 1.33.14 "1.34": 1.34.14 -"1.35": 1.35.10 -"1.36": 1.36.6 -"1.37": 1.37.2 +"1.35": 1.35.13 +"1.36": 1.36.9 +"1.37": 1.37.5 +"1.38": 1.38.3 diff --git a/envoy/access_log/BUILD b/envoy/access_log/BUILD index fda9ca6eb654c..ba815e166f501 100644 --- a/envoy/access_log/BUILD +++ b/envoy/access_log/BUILD @@ -15,9 +15,9 @@ envoy_cc_library( "//envoy/config:typed_config_interface", "//envoy/filesystem:filesystem_interface", "//envoy/formatter:http_formatter_context_interface", - "//envoy/http:header_map_interface", "//envoy/stream_info:stream_info_interface", - "//source/common/protobuf", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", "@envoy_api//envoy/data/accesslog/v3:pkg_cc_proto", ], ) @@ -26,9 +26,11 @@ envoy_cc_library( name = "access_log_config_interface", hdrs = ["access_log_config.h"], deps = [ - "//envoy/access_log:access_log_interface", + ":access_log_interface", + "//envoy/common:pure_lib", + "//envoy/config:typed_config_interface", "//envoy/formatter:substitution_formatter_interface", - "//envoy/server:filter_config_interface", + "//envoy/server:factory_context_interface", "//source/common/protobuf", ], ) diff --git a/envoy/access_log/access_log.h b/envoy/access_log/access_log.h index 2824792c2ae7c..4196b54be561a 100644 --- a/envoy/access_log/access_log.h +++ b/envoy/access_log/access_log.h @@ -1,16 +1,16 @@ #pragma once #include -#include +#include #include "envoy/common/pure.h" #include "envoy/data/accesslog/v3/accesslog.pb.h" #include "envoy/filesystem/filesystem.h" #include "envoy/formatter/http_formatter_context.h" -#include "envoy/http/header_map.h" #include "envoy/stream_info/stream_info.h" -#include "source/common/protobuf/protobuf.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" namespace Envoy { namespace AccessLog { diff --git a/envoy/access_log/access_log_config.h b/envoy/access_log/access_log_config.h index cfe53571282e4..533475264142f 100644 --- a/envoy/access_log/access_log_config.h +++ b/envoy/access_log/access_log_config.h @@ -1,11 +1,13 @@ #pragma once #include +#include #include "envoy/access_log/access_log.h" +#include "envoy/common/pure.h" #include "envoy/config/typed_config.h" #include "envoy/formatter/substitution_formatter.h" -#include "envoy/server/filter_config.h" +#include "envoy/server/factory_context.h" #include "source/common/protobuf/protobuf.h" diff --git a/envoy/api/BUILD b/envoy/api/BUILD index eef9caa255d74..261814abc0851 100644 --- a/envoy/api/BUILD +++ b/envoy/api/BUILD @@ -18,6 +18,7 @@ envoy_cc_library( "//envoy/filesystem:filesystem_interface", "//envoy/server:process_context_interface", "//envoy/stats:custom_stat_namespaces_interface", + "//envoy/stats:stats_interface", "//envoy/thread:thread_interface", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", ], @@ -38,6 +39,5 @@ envoy_cc_library( ], deps = [ "//envoy/network:address_interface", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/api/api.h b/envoy/api/api.h index e567ac51c8e6d..6e65a66ccd3b0 100644 --- a/envoy/api/api.h +++ b/envoy/api/api.h @@ -11,7 +11,7 @@ #include "envoy/filesystem/filesystem.h" #include "envoy/server/process_context.h" #include "envoy/stats/custom_stat_namespaces.h" -#include "envoy/stats/store.h" +#include "envoy/stats/scope.h" #include "envoy/thread/thread.h" namespace Envoy { diff --git a/envoy/api/os_sys_calls.h b/envoy/api/os_sys_calls.h index 08f2227c4f564..582520960f943 100644 --- a/envoy/api/os_sys_calls.h +++ b/envoy/api/os_sys_calls.h @@ -129,6 +129,12 @@ class OsSysCalls { */ virtual bool supportsMptcp() const PURE; + /** + * return true if the OS supports attaching a reuse port BPF program for CPU-based connection + * steering. + */ + virtual bool supportsReusePortBpfCpuSteering() const PURE; + /** * Release all resources allocated for fd. * @return zero on success, -1 returned otherwise. diff --git a/envoy/buffer/buffer.h b/envoy/buffer/buffer.h index 0e0a7f7b2ab8f..c8f82af64a0d0 100644 --- a/envoy/buffer/buffer.h +++ b/envoy/buffer/buffer.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "envoy/api/os_sys_calls.h" @@ -16,8 +17,8 @@ #include "source/common/common/utility.h" #include "absl/container/inlined_vector.h" +#include "absl/functional/any_invocable.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" #include "absl/types/span.h" namespace Envoy { @@ -233,8 +234,7 @@ class Instance { * @param max_slices supplies an optional limit on the number of slices to fetch, for performance. * @return RawSliceVector with non-empty slices in the buffer. */ - virtual RawSliceVector - getRawSlices(absl::optional max_slices = absl::nullopt) const PURE; + virtual RawSliceVector getRawSlices(std::optional max_slices = std::nullopt) const PURE; /** * Fetch the valid data pointer and valid data length of the first non-zero-length @@ -552,9 +552,9 @@ class WatermarkFactory { * high watermark. * @return a newly created InstancePtr. */ - virtual InstancePtr createBuffer(std::function below_low_watermark, - std::function above_high_watermark, - std::function above_overflow_watermark) PURE; + virtual InstancePtr createBuffer(absl::AnyInvocable below_low_watermark, + absl::AnyInvocable above_high_watermark, + absl::AnyInvocable above_overflow_watermark) PURE; /** * Create and returns a buffer memory account. diff --git a/envoy/common/BUILD b/envoy/common/BUILD index 5035c59d5e8a8..8d0b03b74f1a8 100644 --- a/envoy/common/BUILD +++ b/envoy/common/BUILD @@ -39,7 +39,6 @@ envoy_basic_cc_library( hdrs = [ "optref.h", ], - deps = ["@abseil-cpp//absl/types:optional"], ) envoy_cc_library( diff --git a/envoy/common/conn_pool.h b/envoy/common/conn_pool.h index 14c5b05c7c1c0..7ce1ab28a95df 100644 --- a/envoy/common/conn_pool.h +++ b/envoy/common/conn_pool.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/common/pure.h" #include "envoy/event/deferred_deletable.h" #include "envoy/upstream/upstream.h" @@ -54,6 +56,9 @@ enum class DrainBehavior { DrainExistingNonMigratableConnections, }; +class Instance; +using DrainConnectionsPoolPredicate = std::function; + /** * An instance of a generic connection pool. */ @@ -87,6 +92,12 @@ class Instance { */ virtual Upstream::HostDescriptionConstSharedPtr host() const PURE; + /** + * @return const Network::ConnectionSocket::OptionsSharedPtr& the socket + * options used to configure connections in this pool. + */ + virtual const Network::ConnectionSocket::OptionsSharedPtr& socketOptions() PURE; + /** * Creates an upstream connection, if existing connections do not meet both current and * anticipated load. diff --git a/envoy/common/hashable.h b/envoy/common/hashable.h index eb874aa443211..6af37337d529e 100644 --- a/envoy/common/hashable.h +++ b/envoy/common/hashable.h @@ -1,11 +1,10 @@ #pragma once #include +#include #include "envoy/common/pure.h" -#include "absl/types/optional.h" - namespace Envoy { /** @@ -18,10 +17,10 @@ class Hashable { /** * Request the 64-bit hash for this object. - * @return absl::optional the hash value, or absl::nullopt if a hash could not be + * @return std::optional the hash value, or std::nullopt if a hash could not be * produced for this instance. */ - virtual absl::optional hash() const PURE; + virtual std::optional hash() const PURE; }; } // namespace Envoy diff --git a/envoy/common/io/io_uring.h b/envoy/common/io/io_uring.h index 335b8d4c6b064..780b4a5e09d3d 100644 --- a/envoy/common/io/io_uring.h +++ b/envoy/common/io/io_uring.h @@ -42,9 +42,27 @@ class Request { */ IoUringSocket& socket() const { return socket_; } + /** + * The provided buffer id reported by a `multishot` read completion, or -1 when the completion + * does not use a provided buffer. Populated by the io_uring implementation before the completion + * callback runs. + */ + int32_t bufferId() const { return buffer_id_; } + void setBufferId(int32_t buffer_id) { buffer_id_ = buffer_id; } + + /** + * Whether the io_uring will deliver more completions for this request, as with an armed + * `multishot` operation. When true the request is kept alive across completions. Populated by the + * io_uring implementation before the completion callback runs. + */ + bool moreCompletions() const { return more_completions_; } + void setMoreCompletions(bool more_completions) { more_completions_ = more_completions; } + private: RequestType type_; IoUringSocket& socket_; + int32_t buffer_id_{-1}; + bool more_completions_{false}; }; /** @@ -64,6 +82,34 @@ using InjectedCompletionUserDataReleasor = std::function; + /** * Abstract wrapper around `io_uring`. */ @@ -93,6 +139,12 @@ class IoUring { */ virtual void forEveryCompletion(const CompletionCb& completion_cb) PURE; + /** + * Returns true when the completion queue still holds entries. forEveryCompletion reaps a bounded + * batch per call, so the caller checks this to drain a larger burst on a later event loop pass. + */ + virtual bool hasReadyCompletions() const PURE; + /** * Prepares an accept system call and puts it into the submission queue. * Returns IoUringResult::Failed in case the submission queue is full already @@ -118,6 +170,26 @@ class IoUring { virtual IoUringResult prepareReadv(os_fd_t fd, const struct iovec* iovecs, unsigned nr_vecs, off_t offset, Request* user_data) PURE; + /** + * Returns true if `multishot` reads backed by a provided buffer pool are available on this ring. + * When false the caller falls back to readv-based reads. + */ + virtual bool isMultishotEnabled() const PURE; + + /** + * Returns the provided buffer pool used for `multishot` reads, or nullptr when `multishot` reads + * are not available. + */ + virtual IoUringBufferPoolSharedPtr bufferPool() PURE; + + /** + * Prepares a `multishot` recv that draws buffers from the provided buffer pool and puts it into + * the submission queue. The kernel keeps delivering completions until the request is canceled, + * the connection ends or the buffer pool is exhausted. Returns IoUringResult::Failed when the + * submission queue is full already and IoUringResult::Ok otherwise. + */ + virtual IoUringResult prepareReadMultishot(os_fd_t fd, Request* user_data) PURE; + /** * Prepares a writev system call and puts it into the submission queue. * Returns IoUringResult::Failed in case the submission queue is full already @@ -265,7 +337,8 @@ class IoUringSocket { virtual void connect(const Network::Address::InstanceConstSharedPtr& address) PURE; /** - * Write data to the socket. + * Write data to the socket. When the socket is applying write backpressure the data is left in + * the buffer so the caller can retry after a write event is delivered. * @param data is going to write. */ virtual void write(Buffer::Instance& data) PURE; @@ -274,6 +347,8 @@ class IoUringSocket { * Write data to the socket. * @param slices includes the data to write. * @param num_slice the number of slices. + * @return the number of bytes consumed, which is zero when the socket is applying write + * backpressure so the caller can retry after a write event is delivered. */ virtual uint64_t write(const Buffer::RawSlice* slices, uint64_t num_slice) PURE; @@ -361,13 +436,13 @@ class IoUringSocket { /** * Return the data get from the read request. * @return Only return valid ReadParam when the callback is invoked with - * `Event::FileReadyType::Read`, otherwise `absl::nullopt` returned. + * `Event::FileReadyType::Read`, otherwise `std::nullopt` returned. */ virtual const OptRef& getReadParam() const PURE; /** * Return the data get from the write request. * @return Only return valid WriteParam when the callback is invoked with - * `Event::FileReadyType::Write`, otherwise `absl::nullopt` returned. + * `Event::FileReadyType::Write`, otherwise `std::nullopt` returned. */ virtual const OptRef& getWriteParam() const PURE; @@ -458,7 +533,7 @@ class IoUringWorkerFactory { /** * Returns the current thread's IoUringWorker. If the thread have not registered a IoUringWorker, - * an absl::nullopt will be returned. + * an std::nullopt will be returned. */ virtual OptRef getIoUringWorker() PURE; diff --git a/envoy/common/key_value_store.h b/envoy/common/key_value_store.h index 6095a53299f0c..6628a02410ab8 100644 --- a/envoy/common/key_value_store.h +++ b/envoy/common/key_value_store.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/common/pure.h" #include "envoy/config/typed_config.h" @@ -9,7 +10,6 @@ #include "envoy/protobuf/message_validator.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { @@ -27,7 +27,7 @@ class KeyValueStore { * ttl must be greater than 0. */ virtual void addOrUpdate(absl::string_view key, absl::string_view value, - absl::optional ttl) PURE; + std::optional ttl) PURE; /** * Removes a key:value pair from the store. This is a no-op if the key is not present. @@ -38,9 +38,9 @@ class KeyValueStore { /** * Returns the value of the key provided. * @param key supplies a key to return the value of. - * @return the value, if the key is in the store, absl::nullopt otherwise. + * @return the value, if the key is in the store, std::nullopt otherwise. */ - virtual absl::optional get(absl::string_view key) PURE; + virtual std::optional get(absl::string_view key) PURE; /** * Flushes the store to long term storage. diff --git a/envoy/common/optref.h b/envoy/common/optref.h index 0e460cd1bea59..c96a407e4918d 100644 --- a/envoy/common/optref.h +++ b/envoy/common/optref.h @@ -1,6 +1,6 @@ #pragma once -#include "absl/types/optional.h" // required for absl::nullopt +#include // required for std::nullopt namespace Envoy { @@ -11,7 +11,7 @@ namespace Envoy { // } // } // -// Using absl::optional directly you must write optref.value().method() which is +// Using std::optional directly you must write optref.value().method() which is // a bit more awkward. // // This class also consumes less memory -- e.g. 8 bytes for a pointer rather @@ -19,7 +19,7 @@ namespace Envoy { template struct OptRef { OptRef(T& t) : ptr_(&t) {} OptRef() : ptr_(nullptr) {} - OptRef(absl::nullopt_t) : ptr_(nullptr) {} + OptRef(std::nullopt_t) : ptr_(nullptr) {} /** * Copy constructor that allows conversion. @@ -42,7 +42,7 @@ template struct OptRef { * @return a ref to the converted object. */ template operator OptRef() { - return ptr_ == nullptr ? absl::nullopt : OptRef(*ptr_); + return ptr_ == nullptr ? std::nullopt : OptRef(*ptr_); } /** @@ -88,8 +88,8 @@ template struct OptRef { * * @return an optional copy of the referenced object (or nullopt). */ - absl::optional copy() const { - absl::optional ret; + std::optional copy() const { + std::optional ret; if (has_value()) { ret = *ptr_; } @@ -136,7 +136,7 @@ template OptRef makeOptRef(T& ref) { return {ref}; } /** * Constructs an OptRef from the provided pointer. * @param ptr the pointer to wrap - * @return OptRef the wrapped pointer, or absl::nullopt if the pointer is nullptr + * @return OptRef the wrapped pointer, or std::nullopt if the pointer is nullptr */ template OptRef makeOptRefFromPtr(T* ptr) { if (ptr == nullptr) { @@ -146,17 +146,17 @@ template OptRef makeOptRefFromPtr(T* ptr) { return {*ptr}; } -// Overloads for comparing OptRef against absl::nullopt. -template bool operator!=(const OptRef& optref, absl::nullopt_t) { +// Overloads for comparing OptRef against std::nullopt. +template bool operator!=(const OptRef& optref, std::nullopt_t) { return optref.has_value(); } -template bool operator!=(absl::nullopt_t, const OptRef& optref) { +template bool operator!=(std::nullopt_t, const OptRef& optref) { return optref.has_value(); } -template bool operator==(const OptRef& optref, absl::nullopt_t) { +template bool operator==(const OptRef& optref, std::nullopt_t) { return !optref.has_value(); } -template bool operator==(absl::nullopt_t, const OptRef& optref) { +template bool operator==(std::nullopt_t, const OptRef& optref) { return !optref.has_value(); } diff --git a/envoy/common/resource.h b/envoy/common/resource.h index 6af1899039dc2..fed414a73c142 100644 --- a/envoy/common/resource.h +++ b/envoy/common/resource.h @@ -1,11 +1,10 @@ #pragma once #include +#include #include "envoy/common/pure.h" -#include "absl/types/optional.h" - #pragma once namespace Envoy { @@ -48,6 +47,6 @@ class ResourceLimit { virtual uint64_t count() const PURE; }; -using ResourceLimitOptRef = absl::optional>; +using ResourceLimitOptRef = std::optional>; } // namespace Envoy diff --git a/envoy/config/BUILD b/envoy/config/BUILD index 35eacb93dd58e..ad9f959847b4a 100644 --- a/envoy/config/BUILD +++ b/envoy/config/BUILD @@ -15,7 +15,6 @@ envoy_cc_library( "//envoy/common:time_interface", "//source/common/common:assert_lib", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", ], ) @@ -132,7 +131,6 @@ envoy_cc_library( ":typed_config_interface", "//envoy/api:api_interface", "//envoy/protobuf:message_validator_interface", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", ], ) diff --git a/envoy/config/config_provider.h b/envoy/config/config_provider.h index f87e476fcfb8d..07ac73282771f 100644 --- a/envoy/config/config_provider.h +++ b/envoy/config/config_provider.h @@ -1,14 +1,13 @@ #pragma once #include +#include #include "envoy/common/time.h" #include "source/common/common/assert.h" #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Config { @@ -75,16 +74,16 @@ class ConfigProvider { /** * Returns a ConfigProtoInfoVector associated with a ApiType::Delta provider. - * @return absl::optional an optional ConfigProtoInfoVector; the value is + * @return std::optional an optional ConfigProtoInfoVector; the value is * set when a config is available. */ - template absl::optional> configProtoInfoVector() const { + template std::optional> configProtoInfoVector() const { static_assert(std::is_base_of::value, "Proto type must derive from Protobuf::Message"); const ConfigProtoVector config_protos = getConfigProtos(); if (config_protos.empty()) { - return absl::nullopt; + return std::nullopt; } std::vector ret_protos; ret_protos.reserve(config_protos.size()); diff --git a/envoy/config/dynamic_extension_config_provider.h b/envoy/config/dynamic_extension_config_provider.h index 875aae268ccc2..41582972aa0fd 100644 --- a/envoy/config/dynamic_extension_config_provider.h +++ b/envoy/config/dynamic_extension_config_provider.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/common/optref.h" #include "envoy/common/pure.h" #include "envoy/config/extension_config_provider.h" @@ -7,8 +9,6 @@ #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Config { diff --git a/envoy/config/grpc_mux.h b/envoy/config/grpc_mux.h index c97d734be5af4..f8aa1b3e4bf29 100644 --- a/envoy/config/grpc_mux.h +++ b/envoy/config/grpc_mux.h @@ -120,11 +120,13 @@ class GrpcMux { /** * Updates the current gRPC-Mux object to use a new gRPC client, and config. */ - virtual absl::Status - updateMuxSource(Grpc::RawAsyncClientSharedPtr&& primary_async_client, - Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, - BackOffStrategyPtr&& backoff_strategy, - const envoy::config::core::v3::ApiConfigSource& ads_config_source) PURE; + virtual absl::Status updateMuxSource( + Grpc::RawAsyncClientSharedPtr&& primary_async_client, + Grpc::RawAsyncClientSharedPtr&& failover_async_client, Stats::Scope& scope, + BackOffStrategyPtr&& backoff_strategy, + const envoy::config::core::v3::ApiConfigSource& ads_config_source, + std::function()> load_stats_reporter_factory = + nullptr) PURE; /** * Returns a load-stats-reporter that was created for the gRPC-Mux. diff --git a/envoy/config/subscription.h b/envoy/config/subscription.h index bb835a607db53..75e99acaacb82 100644 --- a/envoy/config/subscription.h +++ b/envoy/config/subscription.h @@ -54,7 +54,7 @@ class DecodedResource { */ virtual const Protobuf::Message& resource() const PURE; - virtual absl::optional ttl() const PURE; + virtual std::optional ttl() const PURE; /** * @return bool does the xDS discovery response have a set resource payload? diff --git a/envoy/config/xds_resources_delegate.h b/envoy/config/xds_resources_delegate.h index 407c9059426ce..f19f9d67df949 100644 --- a/envoy/config/xds_resources_delegate.h +++ b/envoy/config/xds_resources_delegate.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/api/api.h" #include "envoy/common/exception.h" #include "envoy/common/optref.h" @@ -10,7 +12,6 @@ #include "envoy/service/discovery/v3/discovery.pb.h" #include "absl/container/flat_hash_set.h" -#include "absl/types/optional.h" namespace Envoy { namespace Config { @@ -82,7 +83,7 @@ class XdsResourcesDelegate { * @param exception The exception that occurred, if any. */ virtual void onResourceLoadFailed(const XdsSourceId& source_id, const std::string& resource_name, - const absl::optional& exception) PURE; + const std::optional& exception) PURE; }; using XdsResourcesDelegatePtr = std::unique_ptr; diff --git a/envoy/content_parser/BUILD b/envoy/content_parser/BUILD index 59739ae11a16a..ca9867f837036 100644 --- a/envoy/content_parser/BUILD +++ b/envoy/content_parser/BUILD @@ -14,7 +14,6 @@ envoy_cc_library( deps = [ "//envoy/common:pure_lib", "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/content_parser/parser.h b/envoy/content_parser/parser.h index a93afddb50b4b..599a0ca6184aa 100644 --- a/envoy/content_parser/parser.h +++ b/envoy/content_parser/parser.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -9,7 +10,6 @@ #include "source/common/protobuf/protobuf.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace ContentParser { @@ -23,7 +23,7 @@ namespace ContentParser { struct MetadataAction { std::string namespace_; // Must be non-empty (parser applies defaults) std::string key; - absl::optional value; // If empty, value extraction failed + std::optional value; // If empty, value extraction failed bool preserve_existing = false; }; @@ -38,7 +38,7 @@ struct ParseResult { bool stop_processing = false; // Error message if parsing failed (e.g., invalid JSON). Empty if no error. - absl::optional error_message; + std::optional error_message; }; /** diff --git a/envoy/event/BUILD b/envoy/event/BUILD index 61fa72d576e48..804ea13d83f7c 100644 --- a/envoy/event/BUILD +++ b/envoy/event/BUILD @@ -32,17 +32,11 @@ envoy_cc_library( "//envoy/common:scope_tracker_interface", "//envoy/common:time_interface", "//envoy/filesystem:watcher_interface", - "//envoy/network:connection_handler_interface", "//envoy/network:connection_interface", - "//envoy/network:dns_interface", "//envoy/network:listen_socket_interface", - "//envoy/network:listener_interface", "//envoy/network:transport_socket_interface", "//envoy/server:watchdog_interface", - "//envoy/server/overload:thread_local_overload_state", - "//envoy/thread:thread_interface", "@abseil-cpp//absl/functional:any_invocable", - "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -63,6 +57,7 @@ envoy_cc_library( name = "scaled_range_timer_manager_interface", hdrs = ["scaled_range_timer_manager.h"], deps = [ + ":dispatcher_interface", ":scaled_timer", ":timer_interface", ], diff --git a/envoy/event/dispatcher.h b/envoy/event/dispatcher.h index 87632969029e6..78ac572749f7c 100644 --- a/envoy/event/dispatcher.h +++ b/envoy/event/dispatcher.h @@ -4,12 +4,9 @@ #include #include #include -#include #include "envoy/common/scope_tracker.h" #include "envoy/common/time.h" -#include "envoy/config/core/v3/resolver.pb.h" -#include "envoy/config/core/v3/udp_socket_config.pb.h" #include "envoy/event/dispatcher_thread_deletable.h" #include "envoy/event/file_event.h" #include "envoy/event/scaled_timer.h" @@ -18,17 +15,11 @@ #include "envoy/event/timer.h" #include "envoy/filesystem/watcher.h" #include "envoy/network/connection.h" -#include "envoy/network/connection_handler.h" -#include "envoy/network/dns.h" #include "envoy/network/listen_socket.h" -#include "envoy/network/listener.h" #include "envoy/network/transport_socket.h" -#include "envoy/server/overload/thread_local_overload_state.h" #include "envoy/server/watchdog.h" #include "envoy/stats/scope.h" -#include "envoy/stats/stats_macros.h" #include "envoy/stream_info/stream_info.h" -#include "envoy/thread/thread.h" #include "absl/functional/any_invocable.h" @@ -185,7 +176,7 @@ class Dispatcher : public DispatcherBase, public ScopeTracker { * identified by its name. */ virtual void initializeStats(Stats::Scope& scope, - const absl::optional& prefix = absl::nullopt) PURE; + const std::optional& prefix = std::nullopt) PURE; /** * Clears any items in the deferred deletion queue. diff --git a/envoy/event/scaled_range_timer_manager.h b/envoy/event/scaled_range_timer_manager.h index d7e86dc542d93..7568f79c5b650 100644 --- a/envoy/event/scaled_range_timer_manager.h +++ b/envoy/event/scaled_range_timer_manager.h @@ -1,13 +1,15 @@ #pragma once +#include +#include + #include "envoy/common/pure.h" +#include "envoy/event/dispatcher.h" #include "envoy/event/scaled_timer.h" #include "envoy/event/timer.h" #include "source/common/common/interval_value.h" -#include "absl/types/variant.h" - namespace Envoy { namespace Event { diff --git a/envoy/event/scaled_timer.h b/envoy/event/scaled_timer.h index 5e17869a01874..9d22b7223bd86 100644 --- a/envoy/event/scaled_timer.h +++ b/envoy/event/scaled_timer.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include "source/common/common/interval_value.h" diff --git a/envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h b/envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h index 7f5ea0c5b71eb..d2481a6ebd349 100644 --- a/envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h +++ b/envoy/extensions/bootstrap/reverse_tunnel/reverse_tunnel_reporter.h @@ -28,9 +28,12 @@ class ReverseTunnelReporter { * @param node_id ID reported by the connecting node. * @param cluster_id cluster which the node belongs to. * @param tenant_id tenant identifier associated with the node. + * @param initiation_time_ms epoch milliseconds when the tunnel was initiated at the downstream + * (DP) envoy. 0 means the timestamp was not provided (e.g. older DP envoys that don't + * send the initiation-time header). */ virtual void reportConnectionEvent(absl::string_view node_id, absl::string_view cluster_id, - absl::string_view tenant_id) PURE; + absl::string_view tenant_id, int64_t initiation_time_ms) PURE; /** * Record that a reverse tunnel has been torn down. diff --git a/envoy/filesystem/filesystem.h b/envoy/filesystem/filesystem.h index 9e02b469a0d1a..f39667ce4aafc 100644 --- a/envoy/filesystem/filesystem.h +++ b/envoy/filesystem/filesystem.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "envoy/api/io_error.h" @@ -12,7 +13,6 @@ #include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Filesystem { @@ -27,14 +27,14 @@ struct FileInfo { const std::string name_; // the size of the file in bytes, or `nullopt` if the size could not be determined // (e.g. for directories, or windows symlinks.) - const absl::optional size_; + const std::optional size_; // Note that if the file represented by name_ is a symlink, type_ will be the file type of the // target. For example, if name_ is a symlink to a directory, its file type will be Directory. // A broken symlink on posix will have `FileType::Regular`. const FileType file_type_; - const absl::optional time_created_; - const absl::optional time_last_accessed_; - const absl::optional time_last_modified_; + const std::optional time_created_; + const std::optional time_last_accessed_; + const std::optional time_last_modified_; }; /** @@ -233,7 +233,7 @@ struct DirectoryEntry { // The file size in bytes for regular files. nullopt for FileType::Directory and FileType::Other, // and, on Windows, also nullopt for symlinks, and on Linux nullopt for broken symlinks. - absl::optional size_bytes_; + std::optional size_bytes_; bool operator==(const DirectoryEntry& rhs) const { return name_ == rhs.name_ && type_ == rhs.type_ && size_bytes_ == rhs.size_bytes_; @@ -246,7 +246,7 @@ class DirectoryIteratorImpl; // and after each increment, if error-handling is desired. class DirectoryIterator { public: - DirectoryIterator() : entry_({"", FileType::Other, absl::nullopt}) {} + DirectoryIterator() : entry_({"", FileType::Other, std::nullopt}) {} virtual ~DirectoryIterator() = default; const DirectoryEntry& operator*() const { return entry_; } diff --git a/envoy/filter/config_provider_manager.h b/envoy/filter/config_provider_manager.h index c4276fa81c616..d132267b5a11f 100644 --- a/envoy/filter/config_provider_manager.h +++ b/envoy/filter/config_provider_manager.h @@ -1,13 +1,13 @@ #pragma once +#include + #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/dynamic_extension_config_provider.h" #include "envoy/init/manager.h" #include "envoy/server/filter_config.h" #include "envoy/upstream/cluster_manager.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Filter { diff --git a/envoy/formatter/BUILD b/envoy/formatter/BUILD index 1bc42b7c6cd18..559a2e388c646 100644 --- a/envoy/formatter/BUILD +++ b/envoy/formatter/BUILD @@ -24,14 +24,15 @@ envoy_cc_library( name = "substitution_formatter_interface", hdrs = [ "substitution_formatter.h", - "substitution_formatter_base.h", ], deps = [ ":http_formatter_context_interface", + "//envoy/common:pure_lib", "//envoy/config:typed_config_interface", - "//envoy/http:header_map_interface", - "//envoy/registry", "//envoy/server:factory_context_interface", "//envoy/stream_info:stream_info_interface", + "//source/common/protobuf", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", ], ) diff --git a/envoy/formatter/substitution_formatter.h b/envoy/formatter/substitution_formatter.h index b43f5270b7e77..51b1c71b513c5 100644 --- a/envoy/formatter/substitution_formatter.h +++ b/envoy/formatter/substitution_formatter.h @@ -1,9 +1,124 @@ #pragma once +#include +#include +#include +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/config/typed_config.h" #include "envoy/formatter/http_formatter_context.h" -#include "envoy/formatter/substitution_formatter_base.h" -#include "envoy/http/header_map.h" +#include "envoy/server/factory_context.h" +#include "envoy/stream_info/stream_info.h" + +#include "source/common/protobuf/protobuf.h" + +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" namespace Envoy { -namespace Formatter {} // namespace Formatter +namespace Formatter { + +/** + * Interface for multiple protocols/modules formatters. + */ +class Formatter { +public: + virtual ~Formatter() = default; + + /** + * Return a formatted substitution line. + * @param context supplies the formatter context. + * @param stream_info supplies the stream info. + * @return std::string string containing the complete formatted substitution line. + */ + virtual std::string format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const PURE; +}; + +using FormatterPtr = std::unique_ptr; +using FormatterConstSharedPtr = std::shared_ptr; + +/** + * Interface for multiple protocols/modules formatter providers. + */ +class FormatterProvider { +public: + virtual ~FormatterProvider() = default; + + /** + * Format the value with the given context and stream info. + * @param context supplies the formatter context. + * @param stream_info supplies the stream info. + * @return std::optional optional string containing a single value extracted from + * the given context and stream info. + */ + virtual std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const PURE; + + /** + * Format the value with the given context and stream info. + * @param context supplies the formatter context. + * @param stream_info supplies the stream info. + * @return Protobuf::Value containing a single value extracted from the given + * context and stream info. + */ + virtual Protobuf::Value formatValue(const Context& context, + const StreamInfo::StreamInfo& stream_info) const PURE; +}; + +using FormatterProviderPtr = std::unique_ptr; + +class CommandParser { +public: + virtual ~CommandParser() = default; + + /** + * Return a FormatterProviderBasePtr if command arg and max_length are correct for the formatter + * provider associated with command. + * @param command command name. + * @param command_arg command specific argument. Empty if no argument is provided. + * @param max_length length to which the output produced by FormatterProvider + * should be truncated to (optional). + * + * @return absl::StatusOr substitution provider for the parsed command or an + * error status. + */ + virtual absl::StatusOr parse(absl::string_view command, + absl::string_view command_arg, + std::optional max_length) const PURE; +}; + +using CommandParserPtr = std::unique_ptr; +using CommandParserPtrVector = std::vector; + +class CommandParserFactory : public Config::TypedFactory { +public: + /** + * Creates a particular CommandParser implementation. + * + * @param config supplies the configuration for the command parser. + * @param context supplies the factory context. + * @return CommandParserPtr the CommandParser which will be used in + * SubstitutionFormatParser::parse() when evaluating an access log format string. + */ + virtual CommandParserPtr + createCommandParserFromProto(const Protobuf::Message& config, + Server::Configuration::GenericFactoryContext& context) PURE; + + std::string category() const override { return "envoy.formatter"; } +}; + +class BuiltInCommandParserFactory : public Config::UntypedFactory { +public: + std::string category() const override { return "envoy.built_in_formatters"; } + + /** + * Creates a particular CommandParser implementation. + */ + virtual CommandParserPtr createCommandParser() const PURE; +}; + +} // namespace Formatter } // namespace Envoy diff --git a/envoy/formatter/substitution_formatter_base.h b/envoy/formatter/substitution_formatter_base.h deleted file mode 100644 index 12c4db28d423f..0000000000000 --- a/envoy/formatter/substitution_formatter_base.h +++ /dev/null @@ -1,144 +0,0 @@ -#pragma once - -#include -#include - -#include "envoy/access_log/access_log.h" -#include "envoy/common/pure.h" -#include "envoy/config/typed_config.h" -#include "envoy/formatter/http_formatter_context.h" -#include "envoy/registry/registry.h" -#include "envoy/server/factory_context.h" -#include "envoy/stream_info/stream_info.h" - -namespace Envoy { -namespace Formatter { - -/** - * Interface for multiple protocols/modules formatters. - */ -class Formatter { -public: - virtual ~Formatter() = default; - - /** - * Return a formatted substitution line. - * @param context supplies the formatter context. - * @param stream_info supplies the stream info. - * @return std::string string containing the complete formatted substitution line. - */ - virtual std::string format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const PURE; -}; - -using FormatterPtr = std::unique_ptr; -using FormatterConstSharedPtr = std::shared_ptr; - -/** - * Interface for multiple protocols/modules formatter providers. - */ -class FormatterProvider { -public: - virtual ~FormatterProvider() = default; - - /** - * Format the value with the given context and stream info. - * @param context supplies the formatter context. - * @param stream_info supplies the stream info. - * @return absl::optional optional string containing a single value extracted from - * the given context and stream info. - */ - virtual absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const PURE; - - /** - * Format the value with the given context and stream info. - * @param context supplies the formatter context. - * @param stream_info supplies the stream info. - * @return Protobuf::Value containing a single value extracted from the given - * context and stream info. - */ - virtual Protobuf::Value formatValue(const Context& context, - const StreamInfo::StreamInfo& stream_info) const PURE; -}; - -using FormatterProviderPtr = std::unique_ptr; - -class CommandParser { -public: - virtual ~CommandParser() = default; - - /** - * Return a FormatterProviderBasePtr if command arg and max_length are correct for the formatter - * provider associated with command. - * @param command command name. - * @param command_arg command specific argument. Empty if no argument is provided. - * @param max_length length to which the output produced by FormatterProvider - * should be truncated to (optional). - * - * @return FormattterProviderPtr substitution provider for the parsed command. - */ - virtual FormatterProviderPtr parse(absl::string_view command, absl::string_view command_arg, - absl::optional max_length) const PURE; -}; - -using CommandParserPtr = std::unique_ptr; -using CommandParserPtrVector = std::vector; - -class CommandParserFactory : public Config::TypedFactory { -public: - /** - * Creates a particular CommandParser implementation. - * - * @param config supplies the configuration for the command parser. - * @param context supplies the factory context. - * @return CommandParserPtr the CommandParser which will be used in - * SubstitutionFormatParser::parse() when evaluating an access log format string. - */ - virtual CommandParserPtr - createCommandParserFromProto(const Protobuf::Message& config, - Server::Configuration::GenericFactoryContext& context) PURE; - - std::string category() const override { return "envoy.formatter"; } -}; - -class BuiltInCommandParserFactory : public Config::UntypedFactory { -public: - std::string category() const override { return "envoy.built_in_formatters"; } - - /** - * Creates a particular CommandParser implementation. - */ - virtual CommandParserPtr createCommandParser() const PURE; -}; - -/** - * Helper class to get all built-in command parsers for a given formatter context. - */ -class BuiltInCommandParserFactoryHelper { -public: - using Factory = BuiltInCommandParserFactory; - using Parsers = std::vector; - - /** - * Get all built-in command parsers for a given formatter context. - * @return Parsers all built-in command parsers for a given formatter context. - */ - static const Parsers& commandParsers() { - CONSTRUCT_ON_FIRST_USE(Parsers, []() { - Parsers parsers; - for (const auto& factory : Envoy::Registry::FactoryRegistry::factories()) { - if (auto parser = factory.second->createCommandParser(); parser == nullptr) { - ENVOY_BUG(false, fmt::format("Null built-in command parser: {}", factory.first)); - continue; - } else { - parsers.push_back(std::move(parser)); - } - } - return parsers; - }()); - } -}; - -} // namespace Formatter -} // namespace Envoy diff --git a/envoy/grpc/BUILD b/envoy/grpc/BUILD index cac24879490ca..54f622c136798 100644 --- a/envoy/grpc/BUILD +++ b/envoy/grpc/BUILD @@ -20,7 +20,6 @@ envoy_cc_library( "//envoy/tracing:tracer_interface", "//source/common/common:assert_lib", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/grpc/async_client.h b/envoy/grpc/async_client.h index 0584da32c6f26..07dd25f24d134 100644 --- a/envoy/grpc/async_client.h +++ b/envoy/grpc/async_client.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/buffer/buffer.h" #include "envoy/common/pure.h" @@ -13,8 +14,6 @@ #include "source/common/common/assert.h" #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Grpc { diff --git a/envoy/grpc/context.h b/envoy/grpc/context.h index a722392f6cb78..be2e6c76f4291 100644 --- a/envoy/grpc/context.h +++ b/envoy/grpc/context.h @@ -31,7 +31,7 @@ class Context { * @param path the request path. * @return the request names, expressed as StatName. */ - virtual absl::optional + virtual std::optional resolveDynamicServiceAndMethod(const Http::HeaderEntry* path) PURE; /** @@ -45,7 +45,7 @@ class Context { * @param path the request path. * @return the request names, expressed as StatName. */ - virtual absl::optional + virtual std::optional resolveDynamicServiceAndMethodWithDotReplaced(const Http::HeaderEntry* path) PURE; /** @@ -56,7 +56,7 @@ class Context { * @param grpc_status supplies the gRPC status. */ virtual void chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, + const std::optional& request_names, const Http::HeaderEntry* grpc_status) PURE; /** @@ -67,7 +67,7 @@ class Context { * @param success supplies whether the call succeeded. */ virtual void chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, bool success) PURE; + const std::optional& request_names, bool success) PURE; /** * Charge a success/failure stat to a cluster/service/method. @@ -76,7 +76,7 @@ class Context { * @param success supplies whether the call succeeded. */ virtual void chargeStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, bool success) PURE; + const std::optional& request_names, bool success) PURE; /** * Charge a request message stat to a cluster/service/method. @@ -85,7 +85,7 @@ class Context { * @param amount supplies the number of the request messages. */ virtual void chargeRequestMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) PURE; /** @@ -95,7 +95,7 @@ class Context { * @param amount supplies the number of the response messages. */ virtual void chargeResponseMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) PURE; /** @@ -105,7 +105,7 @@ class Context { * @param duration supplies the duration of the upstream request. */ virtual void chargeUpstreamStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, std::chrono::milliseconds duration) PURE; /** diff --git a/envoy/http/BUILD b/envoy/http/BUILD index 8baa4937567a7..6d03becfc38a9 100644 --- a/envoy/http/BUILD +++ b/envoy/http/BUILD @@ -35,7 +35,6 @@ envoy_cc_library( "//envoy/tracing:tracer_interface", "//source/common/protobuf", "//source/common/protobuf:utility_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", ], ) @@ -65,6 +64,18 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "client_codec_factory_interface", + hdrs = ["client_codec_factory.h"], + deps = [ + ":codec_interface", + "//envoy/common:random_generator_interface", + "//envoy/network:connection_interface", + "//envoy/network:transport_socket_interface", + "//envoy/upstream:upstream_interface", + ], +) + envoy_cc_library( name = "stream_reset_handler_interface", hdrs = ["stream_reset_handler.h"], @@ -73,7 +84,7 @@ envoy_cc_library( envoy_cc_library( name = "codes_interface", hdrs = ["codes.h"], - deps = ["//envoy/stats:stats_interface"], + deps = ["//envoy/stats:scope_interface"], ) envoy_cc_library( @@ -103,7 +114,6 @@ envoy_cc_library( ":header_map_interface", "//envoy/access_log:access_log_interface", "//envoy/grpc:status", - "@abseil-cpp//absl/types:optional", ], ) @@ -126,7 +136,6 @@ envoy_cc_library( "//envoy/tracing:tracer_interface", "//envoy/upstream:load_balancer_interface", "//source/common/common:scope_tracked_object_stack", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/http/api_listener.h b/envoy/http/api_listener.h index 4d31e96b24cdb..e6a6d9c498fce 100644 --- a/envoy/http/api_listener.h +++ b/envoy/http/api_listener.h @@ -28,7 +28,7 @@ class ApiListener { }; using ApiListenerPtr = std::unique_ptr; -using ApiListenerOptRef = absl::optional>; +using ApiListenerOptRef = std::optional>; } // namespace Http } // namespace Envoy diff --git a/envoy/http/async_client.h b/envoy/http/async_client.h index 6bc6a8eb0b54a..bea3913f75968 100644 --- a/envoy/http/async_client.h +++ b/envoy/http/async_client.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "envoy/buffer/buffer.h" #include "envoy/config/route/v3/route_components.pb.h" @@ -18,8 +19,6 @@ #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Router { class FilterConfig; @@ -279,7 +278,7 @@ class AsyncClient { * A structure to hold the options for AsyncStream object. */ struct StreamOptions { - StreamOptions& setTimeout(const absl::optional& v) { + StreamOptions& setTimeout(const std::optional& v) { timeout = v; return *this; } @@ -347,7 +346,7 @@ class AsyncClient { // will overwrite the older one. StreamOptions& setRetryPolicy(Router::RetryPolicyConstSharedPtr p) { parsed_retry_policy = std::move(p); - retry_policy = absl::nullopt; + retry_policy = std::nullopt; return *this; } StreamOptions& setFilterConfig(const Router::FilterConfigSharedPtr& config) { @@ -378,7 +377,7 @@ class AsyncClient { child_span_name_ = child_span_name; return *this; } - StreamOptions& setSampled(absl::optional sampled) { + StreamOptions& setSampled(std::optional sampled) { sampled_ = sampled; return *this; } @@ -410,7 +409,7 @@ class AsyncClient { // The timeout supplies the stream timeout, measured since when the frame with // end_stream flag is sent until when the first frame is received. - absl::optional timeout; + std::optional timeout; // The buffer_body_for_retry specifies whether the streamed body will be buffered so that // it can be retried. In general, this should be set to false for a true stream. However, @@ -436,9 +435,9 @@ class AsyncClient { // Buffer memory account for tracking bytes. Buffer::BufferMemoryAccountSharedPtr account_{nullptr}; - absl::optional buffer_limit_; + std::optional buffer_limit_; - absl::optional retry_policy; + std::optional retry_policy; Router::RetryPolicyConstSharedPtr parsed_retry_policy; Router::FilterConfigSharedPtr filter_config_; @@ -456,7 +455,7 @@ class AsyncClient { // Only used if parent_span_ is set. std::string child_span_name_{""}; // Sampling decision for the tracing span. The span is sampled by default. - absl::optional sampled_{true}; + std::optional sampled_{true}; // The pointer to sidestream watermark callbacks. Optional, nullptr by default. Http::SidestreamWatermarkCallbacks* sidestream_watermark_callbacks = nullptr; diff --git a/envoy/http/client_codec_factory.h b/envoy/http/client_codec_factory.h new file mode 100644 index 0000000000000..cc1102dd2ce2d --- /dev/null +++ b/envoy/http/client_codec_factory.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include "envoy/common/pure.h" +#include "envoy/common/random_generator.h" +#include "envoy/http/codec.h" +#include "envoy/network/connection.h" +#include "envoy/network/transport_socket.h" +#include "envoy/upstream/upstream.h" + +namespace Envoy { +namespace Http { + +/** + * Per-cluster factory for the upstream (client) HTTP codec. An implementation is attached to a + * cluster via typed_extension_protocol_options and surfaced through + * ClusterInfo::upstreamHttpClientCodecFactory(). + */ +class ClientCodecFactory { +public: + virtual ~ClientCodecFactory() = default; + + /** + * Everything CodecClientProd hands the stock codecs, so that a fresh codec can be constructed. + */ + struct Context { + // The negotiated codec type for this connection. + CodecType type; + // The underlying network connection the codec runs over. + Network::Connection& connection; + // The codec connection callbacks (the owning CodecClient). + ConnectionCallbacks& callbacks; + // The cluster the upstream host belongs to (source of stats, protocol options, header limits). + const Upstream::ClusterInfo& cluster; + // Random generator for codecs that need it (e.g. HTTP/2). + Random::RandomGenerator& random; + // The transport socket options for the connection, if any. Used e.g. by the HTTP/1 codec to + // detect a proxied connection (http11ProxyInfo()). + const std::shared_ptr& options; + }; + + /** + * @param context supplies the materials needed to build a fresh codec. + * @return the client codec to use for this connection, or nullptr to indicate that the stock + * codec CodecClientProd would otherwise build should be used. An implementation that + * wants to decorate the stock behavior constructs its own codec from @p context (the + * stock codec cannot be wrapped after construction, because intercepting inbound events + * such as GOAWAY requires owning the ConnectionCallbacks at construction time). + * + * An implementation that returns a non-null codec takes over the full construction the + * stock path would otherwise perform. In particular, for an HTTP/3 connection it must + * initialize the QUIC session itself (dynamic_cast context.connection to + * EnvoyQuicClientSession and call Initialize()), since CodecClientProd only does so on + * the stock path. Returning nullptr for unsupported codec types defers to the stock + * codec (e.g. the reverse-tunnel codec handles only HTTP/2 and returns nullptr + * otherwise). + */ + virtual ClientConnectionPtr createClientCodec(const Context& context) const PURE; +}; + +} // namespace Http +} // namespace Envoy diff --git a/envoy/http/codec.h b/envoy/http/codec.h index 3a2599120ceb1..6ee9cfd094ceb 100644 --- a/envoy/http/codec.h +++ b/envoy/http/codec.h @@ -21,9 +21,36 @@ #include "source/common/http/status.h" +namespace webtransport { +class Session; +class SessionVisitor; +} // namespace webtransport + namespace Envoy { namespace Http { +// TODO(wbpcode): The webtransport::Session should be used ideally. However, the +// webtransport::Session does not provide the SetVisitor method which is necessary for bridging the +// downstream and upstream session. +class WebTransportSession { +public: + virtual ~WebTransportSession() = default; + + /** + * Set a visitor for this WebTransport session. The visitor will be notified of session-level + * events such as incoming streams and datagrams. This is only used for negotiated WebTransport + * sessions, and should not be set for non-WebTransport sessions. + * + * @param visitor supplies the visitor to set. + */ + virtual void setWebTransportVisitor(std::unique_ptr visitor) PURE; + + /** + * @return a pointer to the webtransport::Session. + */ + virtual webtransport::Session* rawWebTransportSession() PURE; +}; + enum class CodecType { HTTP1, HTTP2, HTTP3 }; namespace Http1 { @@ -75,7 +102,7 @@ class Http1StreamEncoderOptions { }; using Http1StreamEncoderOptionsOptRef = - absl::optional>; + std::optional>; /** * Encodes an HTTP stream. This interface contains methods common to both the request and response @@ -107,7 +134,7 @@ class StreamEncoder { /** * Return the HTTP/1 stream encoder options if applicable. If the stream is not HTTP/1 returns - * absl::nullopt. + * std::nullopt. */ virtual Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() PURE; }; @@ -268,7 +295,7 @@ class RequestDecoder : public virtual StreamDecoder { */ virtual void sendLocalReply(Code code, absl::string_view body, const std::function& modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) PURE; /** @@ -331,6 +358,16 @@ class ResponseDecoder : public virtual StreamDecoder { * the handle. */ virtual ResponseDecoderHandlePtr createResponseDecoderHandle() PURE; + + /** + * @return the WebTransport session of the *downstream* stream paired with this decoder, if any. + * + * Only decoders on the router's upstream path (which are paired 1:1 with a downstream stream) + * implement this; they reach the downstream StreamDecoderFilterCallbacks and return its + * webTransportSession(). It lets the upstream codec obtain the downstream WebTransport session so + * it can bridge the two directly. All other response decoders inherit the default empty OptRef. + */ + virtual OptRef downstreamWebTransportSession() { return {}; } }; /** @@ -465,14 +502,21 @@ class Stream : public StreamResetHandler { virtual const StreamInfo::BytesMeterSharedPtr& bytesMeter() PURE; /** - * @return absl::optional the codec level stream ID if available + * @return std::optional the codec level stream ID if available * or nullopt if not applicable or not yet assigned. * * HTTP/1 streams return nullopt. * HTTP/2 streams return the HTTP/2 stream ID or nullopt if not available. * HTTP/3 streams return the HTTP/3 stream ID or nullopt if not available. */ - virtual absl::optional codecStreamId() const PURE; + virtual std::optional codecStreamId() const PURE; + + /** + * @return the WebTransport session this stream carries, if it is a negotiated WebTransport + * CONNECT stream. Only HTTP/3 codec streams that have created a WebTransportHttp3 session return + * a value; all other streams inherit the default empty OptRef. + */ + virtual OptRef webTransportSession() { return {}; } }; /** @@ -483,9 +527,9 @@ class ReceivedSettings { virtual ~ReceivedSettings() = default; /** - * @return value of SETTINGS_MAX_CONCURRENT_STREAMS, or absl::nullopt if it was not present. + * @return value of SETTINGS_MAX_CONCURRENT_STREAMS, or std::nullopt if it was not present. */ - virtual const absl::optional& maxConcurrentStreams() const PURE; + virtual const std::optional& maxConcurrentStreams() const PURE; }; /** diff --git a/envoy/http/conn_pool.h b/envoy/http/conn_pool.h index e6a5f9eaef292..7853123e0239a 100644 --- a/envoy/http/conn_pool.h +++ b/envoy/http/conn_pool.h @@ -2,7 +2,6 @@ #include #include -#include #include "envoy/common/conn_pool.h" #include "envoy/common/pure.h" @@ -41,11 +40,11 @@ class Callbacks { * @param host supplies the description of the host that will carry the request. For logical * connection pools the description may be different each time this is called. * @param info supplies the stream info object associated with the upstream L4 connection. - * @param protocol supplies the protocol associated with the stream, or absl::nullopt for raw TCP. + * @param protocol supplies the protocol associated with the stream, or std::nullopt for raw TCP. */ virtual void onPoolReady(RequestEncoder& encoder, Upstream::HostDescriptionConstSharedPtr host, StreamInfo::StreamInfo& info, - absl::optional protocol) PURE; + std::optional protocol) PURE; }; class Instance; @@ -120,9 +119,6 @@ class Instance : public Envoy::ConnectionPool::Instance, public Event::DeferredD * @return absl::string_view a protocol description for logging. */ virtual absl::string_view protocolDescription() const PURE; - - virtual void setLifetimeCallbacks(OptRef callbacks, - std::vector hash_key) PURE; }; using InstancePtr = std::unique_ptr; diff --git a/envoy/http/filter.h b/envoy/http/filter.h index ef4d2aa7da8e5..81ac89220a94a 100644 --- a/envoy/http/filter.h +++ b/envoy/http/filter.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "envoy/access_log/access_log.h" @@ -23,8 +24,6 @@ #include "source/common/common/scope_tracked_object_stack.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Router { class RouteConfigProvider; @@ -212,6 +211,16 @@ class UpstreamCallbacks { public: virtual ~UpstreamCallbacks() = default; + // Called when a host has been selected for the upstream connection but BEFORE the connection + // is initiated. This allows upstream HTTP filters to inspect the selected host and reject + // the connection if needed (e.g., blocking private IP addresses). + // + // To reject the connection, the filter should call sendLocalReply() which will abort the + // upstream connection attempt. + // + // @param host the selected upstream host. + virtual void onHostSelected(const Upstream::HostDescriptionConstSharedPtr& host) PURE; + // Called when the upstream connection is established and // UpstreamStreamFilterCallbacks::upstream should be available. // @@ -228,6 +237,11 @@ class UpstreamStreamFilterCallbacks { // Returns a handle to the upstream stream's stream info. virtual StreamInfo::StreamInfo& upstreamStreamInfo() PURE; + // Returns the WebTransport session of the *downstream* stream paired with this upstream request, + // if any. Used by the upstream WebTransport codec to obtain the downstream session so it can + // bridge the two. Defaults to an empty OptRef for non-WebTransport streams. + virtual OptRef downstreamWebTransportSession() { return {}; } + // Returns a handle to the generic upstream. virtual OptRef upstream() PURE; @@ -304,7 +318,7 @@ class RouteConfigUpdateRequester { virtual void requestRouteConfigUpdate(RouteCache& route_cache, Http::RouteConfigUpdatedCallbackSharedPtr route_config_updated_cb, - absl::optional route_config, + std::optional route_config, Event::Dispatcher& dispatcher, RequestHeaderMap& request_headers) PURE; virtual void requestVhdsUpdate(const std::string& host_header, Event::Dispatcher& thread_local_dispatcher, @@ -345,7 +359,7 @@ class DownstreamStreamFilterCallbacks { * refreshCachedRoute. It is important to note that setRoute(nullptr) is different from a * clearRouteCache(), because clearRouteCache() wants route resolution to be attempted again. * clearRouteCache() achieves this by setting cached_route_ and cached_cluster_info_ to - * absl::optional ptrs instead of null ptrs. + * std::optional ptrs instead of null ptrs. */ virtual void setRoute(Router::RouteConstSharedPtr route) PURE; @@ -391,6 +405,14 @@ class DownstreamStreamFilterCallbacks { */ virtual void refreshRouteCluster() PURE; + /** + * Re-initialize the cached cluster info using the currently selected target cluster + * name from the matched route, without invoking cluster re-selection in specifiers. + * Useful to pull newly fetched cluster metadata (like from on-demand discovery) into + * the active stream. + */ + virtual void recreateClusterInfo() PURE; + /** * Schedules a request for a RouteConfiguration update from the management server. * @param route_config_updated_cb callback to be called when the configuration update has been @@ -510,7 +532,7 @@ class StreamFilterCallbacks { /** * Return the HTTP/1 stream encoder options if applicable. If the stream is not HTTP/1 returns - * absl::nullopt. + * std::nullopt. */ virtual Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() PURE; @@ -592,6 +614,17 @@ class StreamFilterCallbacks { */ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { public: + /** + * @return the WebTransport session of the stream this decoder filter chain is bound to, if any. + * + * For the downstream HTTP connection manager this returns the downstream codec stream's + * WebTransport session. It lets the upstream WebTransport codec obtain the downstream session + * (via the router/upstream-callbacks chain) and bridge the two directly, without going through + * filter state. Filters/chains not carrying a WebTransport session inherit the default empty + * OptRef. + */ + virtual OptRef webTransportSession() { return {}; } + /** * Continue iterating through the filter chain with buffered headers and body data. This routine * can only be called if the filter has previously returned StopIteration from decodeHeaders() @@ -705,7 +738,7 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void sendLocalReply(Code response_code, absl::string_view body_text, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) PURE; /** @@ -916,7 +949,7 @@ class StreamFilterBase { // The error code which (barring reset) will be sent to the client. Http::Code code_; // The gRPC status set in local reply. - absl::optional grpc_status_; + std::optional grpc_status_; // The details of why a local reply is being sent. absl::string_view details_; // True if a reset will occur rather than the local reply (some prior filter @@ -1122,7 +1155,7 @@ class StreamEncoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void sendLocalReply(Code response_code, absl::string_view body_text, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) PURE; /** * Adds new metadata to be encoded. @@ -1226,18 +1259,43 @@ class StreamFilter : public virtual StreamDecoderFilter, public virtual StreamEn using StreamFilterSharedPtr = std::shared_ptr; -class HttpMatchingData { +class HttpMatchingData final { public: static absl::string_view name() { return "http"; } - virtual ~HttpMatchingData() = default; + explicit HttpMatchingData(const StreamInfo::StreamInfo& stream_info) + : stream_info_(stream_info) {} - virtual RequestHeaderMapOptConstRef requestHeaders() const PURE; - virtual RequestTrailerMapOptConstRef requestTrailers() const PURE; - virtual ResponseHeaderMapOptConstRef responseHeaders() const PURE; - virtual ResponseTrailerMapOptConstRef responseTrailers() const PURE; - virtual const StreamInfo::StreamInfo& streamInfo() const PURE; - virtual const Network::ConnectionInfoProvider& connectionInfoProvider() const PURE; + void onRequestHeaders(const RequestHeaderMap& request_headers) { + request_headers_ = &request_headers; + } + + void onRequestTrailers(const RequestTrailerMap& request_trailers) { + request_trailers_ = &request_trailers; + } + + void onResponseHeaders(const ResponseHeaderMap& response_headers) { + response_headers_ = &response_headers; + } + + void onResponseTrailers(const ResponseTrailerMap& response_trailers) { + response_trailers_ = &response_trailers; + } + + RequestHeaderMapOptConstRef requestHeaders() const { return makeOptRefFromPtr(request_headers_); } + RequestTrailerMapOptConstRef requestTrailers() const { + return makeOptRefFromPtr(request_trailers_); + } + ResponseHeaderMapOptConstRef responseHeaders() const { + return makeOptRefFromPtr(response_headers_); + } + ResponseTrailerMapOptConstRef responseTrailers() const { + return makeOptRefFromPtr(response_trailers_); + } + const StreamInfo::StreamInfo& streamInfo() const { return stream_info_; } + const Network::ConnectionInfoProvider& connectionInfoProvider() const { + return stream_info_.downstreamAddressProvider(); + } const StreamInfo::FilterState& filterState() const { return streamInfo().filterState(); } const envoy::config::core::v3::Metadata& metadata() const { @@ -1253,6 +1311,13 @@ class HttpMatchingData { } Ssl::ConnectionInfoConstSharedPtr ssl() const { return connectionInfoProvider().sslConnection(); } + +private: + const StreamInfo::StreamInfo& stream_info_; + const RequestHeaderMap* request_headers_{}; + const ResponseHeaderMap* response_headers_{}; + const RequestTrailerMap* request_trailers_{}; + const ResponseTrailerMap* response_trailers_{}; }; /** @@ -1323,9 +1388,9 @@ class FilterChainFactoryCallbacks { * Check whether the filter chain is disabled for this stream. * @param name the name of the filter chain to check. * - * @return absl::optional whether the filter chain is disabled for this stream. + * @return std::optional whether the filter chain is disabled for this stream. */ - virtual absl::optional filterDisabled(absl::string_view name) const PURE; + virtual std::optional filterDisabled(absl::string_view name) const PURE; /** * @return const StreamInfo::StreamInfo& the stream info for this stream. diff --git a/envoy/http/filter_factory.h b/envoy/http/filter_factory.h index 8f421840a4ee5..e9f29997bb541 100644 --- a/envoy/http/filter_factory.h +++ b/envoy/http/filter_factory.h @@ -2,11 +2,11 @@ #include #include +#include #include "envoy/common/pure.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Http { @@ -29,10 +29,14 @@ using FilterFactoryCb = std::function + #include "envoy/http/header_map.h" #include "envoy/stream_info/stream_info.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Http { @@ -41,12 +41,12 @@ class HashPolicy { * @param info stores the stream info for the stream. * @param add_cookie is called to add a set-cookie header on the reply sent to the downstream * host. - * @return absl::optional an optional hash value to route on. A hash value might not be + * @return std::optional an optional hash value to route on. A hash value might not be * returned if for example the specified HTTP header does not exist. */ - virtual absl::optional generateHash(OptRef headers, - OptRef info, - AddCookieCallback add_cookie = nullptr) const PURE; + virtual std::optional generateHash(OptRef headers, + OptRef info, + AddCookieCallback add_cookie = nullptr) const PURE; }; } // namespace Http diff --git a/envoy/http/header_map.h b/envoy/http/header_map.h index 356c8d94d2725..ad10aa689c59c 100644 --- a/envoy/http/header_map.h +++ b/envoy/http/header_map.h @@ -646,14 +646,14 @@ class CustomInlineHeaderRegistry { * Fetch the handle for a registered inline header. May only be called after finalized(). */ template - static absl::optional> getInlineHeader(const LowerCaseString& header_name) { + static std::optional> getInlineHeader(const LowerCaseString& header_name) { ASSERT(mutableFinalized()); auto& map = mutableRegistrationMap(); auto entry = map.find(header_name); if (entry != map.end()) { return Handle(entry); } - return absl::nullopt; + return std::nullopt; } /** diff --git a/envoy/http/original_ip_detection.h b/envoy/http/original_ip_detection.h index e697ba486e2ff..03dfbb8e9faf3 100644 --- a/envoy/http/original_ip_detection.h +++ b/envoy/http/original_ip_detection.h @@ -38,7 +38,7 @@ struct OriginalIPDetectionResult { bool allow_trusted_address_checks; // If set, these parameters will be used to signal that detection failed and the request should // be rejected. - absl::optional reject_options; + std::optional reject_options; // Whether to skip appending the detected remote address to ``x-forwarded-for``. bool skip_xff_append; }; diff --git a/envoy/http/query_params.h b/envoy/http/query_params.h index b41530d67c8c3..41c7a5216b66f 100644 --- a/envoy/http/query_params.h +++ b/envoy/http/query_params.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -8,7 +9,6 @@ #include "absl/container/btree_map.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Http { @@ -26,7 +26,7 @@ class QueryParamsMulti { void overwrite(absl::string_view key, absl::string_view value); std::string toString() const; std::string replaceQueryString(const HeaderString& path) const; - absl::optional getFirstValue(absl::string_view key) const; + std::optional getFirstValue(absl::string_view key) const; const absl::btree_map>& data() const { return data_; } diff --git a/envoy/http/request_id_extension.h b/envoy/http/request_id_extension.h index 828c7123aabab..4099c9f4e864d 100644 --- a/envoy/http/request_id_extension.h +++ b/envoy/http/request_id_extension.h @@ -22,7 +22,7 @@ class RequestIDExtension { * @param request_headers supplies the incoming request headers for retrieving the request ID. * @return the string view or nullopt if the request ID is invalid. */ - virtual absl::optional + virtual std::optional get(const Http::RequestHeaderMap& request_headers) const PURE; /** @@ -31,7 +31,7 @@ class RequestIDExtension { * @param request_headers supplies the incoming request headers for retrieving the request ID. * @return the integer or nullopt if the request ID is invalid. */ - virtual absl::optional + virtual std::optional getInteger(const Http::RequestHeaderMap& request_headers) const PURE; /** diff --git a/envoy/http/stateful_session.h b/envoy/http/stateful_session.h index 078d29f753e19..60dc79c1b7919 100644 --- a/envoy/http/stateful_session.h +++ b/envoy/http/stateful_session.h @@ -23,10 +23,10 @@ class SessionState { /** * Get address of upstream host that the current session stuck on. * - * @return absl::optional optional upstream address. If there is no available - * session or no available address, absl::nullopt will be returned. + * @return std::optional optional upstream address. If there is no available + * session or no available address, std::nullopt will be returned. */ - virtual absl::optional upstreamAddress() const PURE; + virtual std::optional upstreamAddress() const PURE; /** * Called when a request is completed to update the session state. diff --git a/envoy/http/stream_reset_handler.h b/envoy/http/stream_reset_handler.h index 09e3b1dd80383..74d5cd862f81e 100644 --- a/envoy/http/stream_reset_handler.h +++ b/envoy/http/stream_reset_handler.h @@ -39,6 +39,10 @@ enum class StreamResetReason { OverloadManager, // If stream was locally reset due to HTTP/1 upstream half closing before downstream. Http1PrematureUpstreamHalfClose, + // If a remote RST_STREAM(NO_ERROR) was received after a complete response. + // Per RFC 9113 Section 8.1 (HTTP/2) and RFC 9114 Section 4.1 (HTTP/3), this is not an error, and + // the client should not discard the response. + RemoteResetNoError, }; /** diff --git a/envoy/matcher/matcher.h b/envoy/matcher/matcher.h index 9fb829c1807da..487e7234c0a44 100644 --- a/envoy/matcher/matcher.h +++ b/envoy/matcher/matcher.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/common/optref.h" @@ -16,7 +17,6 @@ #include "absl/container/flat_hash_set.h" #include "absl/functional/overload.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" #include "absl/types/variant.h" #include "xds/type/matcher/v3/matcher.pb.h" @@ -83,6 +83,7 @@ enum class MatchResult { }; // Prints a human-readable string representing the MatchResult. +// NOLINTNEXTLINE(readability-identifier-naming) inline static std::string MatchResultToString(MatchResult match_result) { switch (match_result) { case MatchResult::Matched: @@ -142,12 +143,12 @@ template class OnMatchFactory { virtual ~OnMatchFactory() = default; // Instantiates a nested matcher sub-tree or an action. - // Returns absl::nullopt if neither sub-tree or action is specified. - virtual absl::optional> + // Returns std::nullopt if neither sub-tree or action is specified. + virtual std::optional> createOnMatch(const xds::type::matcher::v3::Matcher::OnMatch&) PURE; // Instantiates a nested matcher sub-tree or an action. - // Returns absl::nullopt if neither sub-tree or action is specified. - virtual absl::optional> + // Returns std::nullopt if neither sub-tree or action is specified. + virtual std::optional> createOnMatch(const envoy::config::common::matcher::v3::Matcher::OnMatch&) PURE; }; @@ -216,7 +217,7 @@ template class MatchTree { // Internally handle recursion & keep_matching logic in matcher implementations. // This should be called against initial matching & on-no-match results. static inline ActionMatchResult - handleRecursionAndSkips(const absl::optional>& on_match, const DataType& data, + handleRecursionAndSkips(const std::optional>& on_match, const DataType& data, SkippedMatchCb skipped_match_cb) { if (!on_match.has_value()) { return ActionMatchResult::noMatch(); @@ -313,14 +314,12 @@ class DataInputGetResult : public NonCopyable { /** * @return the default "string" data or nil. Life time must be bound by "this". */ - absl::optional stringData() const { + std::optional stringData() const { return absl::visit( absl::Overload{ - [](const std::string& arg) { return absl::make_optional(arg); }, - [](const absl::string_view& arg) { - return absl::make_optional(arg); - }, - [](const auto&) { return absl::optional(); }}, + [](const std::string& arg) { return std::make_optional(arg); }, + [](const absl::string_view& arg) { return std::make_optional(arg); }, + [](const auto&) { return std::optional(); }}, data_); } @@ -337,8 +336,9 @@ class DataInputGetResult : public NonCopyable { } static DataInputGetResult + // NOLINTNEXTLINE(readability-identifier-naming) NoData(DataAvailability data_availability = DataAvailability::AllDataAvailable) { - return DataInputGetResult(absl::monostate(), data_availability); + return {absl::monostate(), data_availability}; } /** @@ -346,21 +346,24 @@ class DataInputGetResult : public NonCopyable { *duration of matching. Use CreateString when a string must be constructed. **/ static DataInputGetResult + // NOLINTNEXTLINE(readability-identifier-naming) CreateStringView(absl::string_view data, DataAvailability data_availability = DataAvailability::AllDataAvailable) { - return DataInputGetResult(data, data_availability); + return {data, data_availability}; } static DataInputGetResult + // NOLINTNEXTLINE(readability-identifier-naming) CreateString(std::string&& data, DataAvailability data_availability = DataAvailability::AllDataAvailable) { - return DataInputGetResult(std::move(data), data_availability); + return {std::move(data), data_availability}; } static DataInputGetResult + // NOLINTNEXTLINE(readability-identifier-naming) CreateCustom(std::shared_ptr&& data, DataAvailability data_availability = DataAvailability::AllDataAvailable) { - return DataInputGetResult(std::move(data), data_availability); + return {std::move(data), data_availability}; } private: @@ -484,7 +487,7 @@ template class CustomMatcherFactory : public Config::TypedFacto createCustomMatcherFactoryCb(const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& factory_context, DataInputFactoryCb data_input, - absl::optional> on_no_match, + std::optional> on_no_match, OnMatchFactory& on_match_factory) PURE; std::string category() const override { // Static assert to guide implementors to understand what is required. diff --git a/envoy/network/BUILD b/envoy/network/BUILD index be56ae3afa56b..6dc414b74e9e6 100644 --- a/envoy/network/BUILD +++ b/envoy/network/BUILD @@ -57,7 +57,7 @@ envoy_cc_library( ":listener_interface", "//envoy/common:random_generator_interface", "//envoy/runtime:runtime_interface", - "//envoy/ssl:context_interface", + "//envoy/server/overload:thread_local_overload_state", "//source/common/common:interval_value", ], ) @@ -85,6 +85,17 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "udp_packet_writer_factory_factory_interface", + hdrs = ["udp_packet_writer_factory_factory.h"], + deps = [ + ":udp_packet_writer_handler_interface", + "//envoy/config:typed_config_interface", + "//envoy/server:factory_context_interface", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "dns_interface", hdrs = ["dns.h"], @@ -98,6 +109,7 @@ envoy_cc_library( name = "dns_resolver_interface", hdrs = ["dns_resolver.h"], deps = [ + ":dns_interface", "//envoy/api:api_interface", "//source/common/config:utility_lib", ], @@ -140,7 +152,6 @@ envoy_cc_library( hdrs = ["hash_policy.h"], deps = [ ":connection_interface", - "@abseil-cpp//absl/types:optional", ], ) @@ -155,7 +166,6 @@ envoy_cc_library( "//envoy/event:file_event_interface", "//source/common/buffer:buffer_lib", "//source/common/common:assert_lib", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/network/address.h b/envoy/network/address.h index 827e352acd5c8..d4a308b85ddf1 100644 --- a/envoy/network/address.h +++ b/envoy/network/address.h @@ -283,7 +283,7 @@ class Instance { /** * @return filepath of the network namespace for the address. */ - virtual absl::optional networkNamespace() const PURE; + virtual std::optional networkNamespace() const PURE; /** * @return a copy of the address with the linux network namespace overridden for IPv4/v6 diff --git a/envoy/network/connection.h b/envoy/network/connection.h index 393ffa2d5a1d6..7acfb2b7ea73d 100644 --- a/envoy/network/connection.h +++ b/envoy/network/connection.h @@ -61,6 +61,17 @@ class ConnectionCallbacks { * watermark to under its low watermark. */ virtual void onBelowWriteBufferLowWatermark() PURE; + + /** + * Called when the connection has been notified that it is being drained (for example + * because the filter chain or listener that owns it is being drained). Callbacks may + * use this signal to begin a graceful shutdown of any state layered on top of the + * connection (e.g. send an HTTP/2 GOAWAY or close-after-current-request). + * + * The connection remains usable after this call; it is not closed by onDrain() itself. + * The default implementation is a no-op. + */ + virtual void onDrain() {} }; /** @@ -166,6 +177,14 @@ class Connection : public Event::DeferredDeletable, */ virtual bool isHalfCloseEnabled() const PURE; + /** + * Notify the connection that it is being drained. This will fire onDrain() on every + * registered ConnectionCallbacks. Drain is a notification only: the connection is + * not closed and remains usable. Callbacks may react by initiating a graceful + * shutdown of any higher-level state (e.g. HTTP/2 GOAWAY). + */ + virtual void onDrain() PURE; + /** * Close the connection. * @param type the connection close type. @@ -270,7 +289,7 @@ class Connection : public Event::DeferredDeletable, * @return The unix socket peer credentials of the remote client. Note that this is only * supported for unix socket connections. */ - virtual absl::optional unixSocketPeerCredentials() const PURE; + virtual std::optional unixSocketPeerCredentials() const PURE; /** * Set the stats to update for various connection state changes. Note that for performance reasons @@ -397,11 +416,11 @@ class Connection : public Event::DeferredDeletable, virtual bool startSecureTransport() PURE; /** - * @return absl::optional An optional of the most recent round-trip + * @return std::optional An optional of the most recent round-trip * time of the connection. If the platform does not support this, then an empty optional is * returned. */ - virtual absl::optional lastRoundTripTime() const PURE; + virtual std::optional lastRoundTripTime() const PURE; /** * Try to configure the connection's initial congestion window. @@ -424,7 +443,7 @@ class Connection : public Event::DeferredDeletable, * @note some congestion controller's cwnd is measured in number of packets, in that case the * return value is cwnd(in packets) times the connection's MSS. */ - virtual absl::optional congestionWindowInBytes() const PURE; + virtual std::optional congestionWindowInBytes() const PURE; }; using ConnectionPtr = std::unique_ptr; diff --git a/envoy/network/connection_balancer.h b/envoy/network/connection_balancer.h index bb83a07cf3129..72d7495d04046 100644 --- a/envoy/network/connection_balancer.h +++ b/envoy/network/connection_balancer.h @@ -49,7 +49,7 @@ listener. */ virtual void onAcceptWorker(Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections, bool rebalanced, - const absl::optional& network_namespace) PURE; + const std::optional& network_namespace) PURE; }; /** @@ -86,7 +86,7 @@ class ConnectionBalancer { using ConnectionBalancerSharedPtr = std::shared_ptr; using BalancedConnectionHandlerOptRef = - absl::optional>; + std::optional>; } // namespace Network } // namespace Envoy diff --git a/envoy/network/connection_handler.h b/envoy/network/connection_handler.h index ed665428b6309..0dc78ddad0ab0 100644 --- a/envoy/network/connection_handler.h +++ b/envoy/network/connection_handler.h @@ -12,7 +12,6 @@ #include "envoy/network/listener.h" #include "envoy/runtime/runtime.h" #include "envoy/server/overload/thread_local_overload_state.h" -#include "envoy/ssl/context.h" #include "source/common/common/interval_value.h" @@ -69,7 +68,7 @@ class ConnectionHandler { * @param runtime the runtime for the server. * @param random a random number generator. */ - virtual void addListener(absl::optional overridden_listener, ListenerConfig& config, + virtual void addListener(std::optional overridden_listener, ListenerConfig& config, Runtime::Loader& runtime, Random::RandomGenerator& random) PURE; /** @@ -99,6 +98,23 @@ class ConnectionHandler { virtual void stopListeners(uint64_t listener_tag, const Network::ExtraShutdownListenerOptions& options) PURE; + /** + * Notify the connections in the given filter chains that they are being drained. This is + * intended to be invoked at the start of a filter-chain drain sequence (before the drain + * timer expires and connections are forcibly closed). It does not close connections. + * @param listener_tag supplies the tag passed to addListener(). + * @param filter_chains supplies the filter chains whose connections should be notified. + */ + virtual void onFilterChainDrain(uint64_t listener_tag, + const std::list& filter_chains) PURE; + + /** + * Notify all connections belonging to the listener with the given tag that they are being + * drained. Does not close any connections. + * @param listener_tag supplies the tag passed to addListener(). + */ + virtual void onListenerDrain(uint64_t listener_tag) PURE; + /** * Stop all listeners. This will not close any connections and is used for draining. */ @@ -127,6 +143,11 @@ class ConnectionHandler { */ virtual const std::string& statPrefix() const PURE; + /** + * Close idle HTTP connections. + */ + virtual void closeIdleHttpConnections(bool is_saturated) PURE; + /** * Used by ConnectionHandler to manage listeners. */ @@ -172,6 +193,28 @@ class ConnectionHandler { */ virtual void onFilterChainDraining( const std::list& draining_filter_chains) PURE; + + /** + * Called at the start of the drain sequence for the given filter chains. Implementations + * should notify all owned connections that they are being drained (by invoking + * Network::Connection::onDrain()) so that callbacks registered on those connections can + * react to the drain (e.g. send GOAWAY). Connections are not closed by this call. + */ + virtual void onFilterChainDrainStart( + const std::list& draining_filter_chains) PURE; + + /** + * Called at the start of the drain sequence for the listener as a whole. Implementations + * should notify all owned connections that they are being drained. Connections are not + * closed by this call. + */ + virtual void onListenerDrainStart() PURE; + + // New method for handling idle connection closing + virtual void onCloseIdleHttpConnections(bool /*is_saturated*/) { + // Default implementation does nothing. + // Specific listener types (TCP, QUIC) will override this. + } }; using ActiveListenerPtr = std::unique_ptr; @@ -243,7 +286,7 @@ class UdpConnectionHandler : public virtual ConnectionHandler { public: /** * Get the ``UdpListenerCallbacks`` associated with ``listener_tag`` and ``address``. This will be - * absl::nullopt for non-UDP listeners and for ``listener_tag`` values that have already been + * std::nullopt for non-UDP listeners and for ``listener_tag`` values that have already been * removed. */ virtual UdpListenerCallbacksOptRef @@ -320,7 +363,7 @@ class InternalListenerManager { }; using InternalListenerManagerOptRef = - absl::optional>; + std::optional>; // The thread local registry. class LocalInternalListenerRegistry { diff --git a/envoy/network/dns.h b/envoy/network/dns.h index 3b5fcf893c499..4e42ae78c77d2 100644 --- a/envoy/network/dns.h +++ b/envoy/network/dns.h @@ -4,13 +4,13 @@ #include #include #include +#include #include #include "envoy/common/pure.h" #include "envoy/common/time.h" #include "envoy/network/address.h" -#include "absl/types/optional.h" #include "absl/types/variant.h" namespace Envoy { diff --git a/envoy/network/filter.h b/envoy/network/filter.h index 6a96af5fed27b..a863eae82e54c 100644 --- a/envoy/network/filter.h +++ b/envoy/network/filter.h @@ -116,6 +116,18 @@ class WriteFilterCallbacks : public virtual NetworkFilterCallbacks { * marking the filter as close ready. */ virtual void disableClose(bool disabled) PURE; + + /** + * Called by a write filter to signal that its internal buffer is above its high watermark. + * This propagates backpressure to the connection, which may notify ConnectionCallbacks. + */ + virtual void onAboveWriteBufferHighWatermark() PURE; + + /** + * Called by a write filter to signal that its internal buffer has drained below its low + * watermark. Must be paired with a prior onAboveWriteBufferHighWatermark() call. + */ + virtual void onBelowWriteBufferLowWatermark() PURE; }; /** diff --git a/envoy/network/hash_policy.h b/envoy/network/hash_policy.h index c7d62591c1137..92f482e3e53f0 100644 --- a/envoy/network/hash_policy.h +++ b/envoy/network/hash_policy.h @@ -1,8 +1,8 @@ #pragma once -#include "envoy/network/connection.h" +#include -#include "absl/types/optional.h" +#include "envoy/network/connection.h" namespace Envoy { namespace Network { @@ -17,11 +17,11 @@ class HashPolicy { * @param connection is the raw downstream connection. Different implementations of HashPolicy can * compute hashes based on different data accessible from the connection (e.g. IP address, * filter state, etc.). - * @return absl::optional an optional hash value to route on. A hash value might not be + * @return std::optional an optional hash value to route on. A hash value might not be * returned if the hash policy implementation doesn't find the expected data in the connection * (e.g. IP address is null, filter state is not populated, etc.). */ - virtual absl::optional generateHash(const Network::Connection& connection) const PURE; + virtual std::optional generateHash(const Network::Connection& connection) const PURE; }; } // namespace Network } // namespace Envoy diff --git a/envoy/network/io_handle.h b/envoy/network/io_handle.h index cd2c2f1e5c069..b99c374b04ba0 100644 --- a/envoy/network/io_handle.h +++ b/envoy/network/io_handle.h @@ -2,6 +2,7 @@ #include #include +#include #include "envoy/api/io_error.h" #include "envoy/api/os_sys_calls_common.h" @@ -14,7 +15,6 @@ #include "source/common/buffer/buffer_impl.h" #include "absl/container/fixed_array.h" -#include "absl/types/optional.h" namespace Envoy { namespace Buffer { @@ -87,13 +87,13 @@ class IoHandle { /** * Read from a io handle directly into buffer. * @param buffer supplies the buffer to read into. - * @param max_length supplies the maximum length to read. A value of absl::nullopt means to read + * @param max_length supplies the maximum length to read. A value of std::nullopt means to read * as much data as possible, within the constraints of available buffer size. * @return a IoCallUint64Result with err_ = nullptr and rc_ = the number of bytes * read if successful, or err_ = some IoError for failure. If call failed, rc_ shouldn't be used. */ virtual Api::IoCallUint64Result read(Buffer::Instance& buffer, - absl::optional max_length) PURE; + std::optional max_length) PURE; /** * Write the data in slices out. @@ -173,8 +173,8 @@ class IoHandle { // Struct representation of QuicProtocolOptions::SaveCmsgConfig config proto. struct UdpSaveCmsgConfig { - absl::optional level; - absl::optional type; + std::optional level; + std::optional type; uint32_t expected_size = 0; bool hasConfig() const { return (level.has_value() && type.has_value()); } @@ -291,7 +291,7 @@ class IoHandle { /** * @return the domain used by underlying socket (see man 2 socket) */ - virtual absl::optional domain() PURE; + virtual std::optional domain() PURE; /** * Get local address (ip:port pair) @@ -348,11 +348,11 @@ class IoHandle { virtual Api::SysCallIntResult shutdown(int how) PURE; /** - * @return absl::optional An optional of the most recent round-trip + * @return std::optional An optional of the most recent round-trip * time of the connection. If the platform does not support this, then an empty optional is * returned. */ - virtual absl::optional lastRoundTripTime() PURE; + virtual std::optional lastRoundTripTime() PURE; /** * @return the current congestion window in bytes, or unset if not available or not @@ -360,12 +360,12 @@ class IoHandle { * @note some congestion controller's cwnd is measured in number of packets, in that case the * return value is cwnd(in packets) times the connection's MSS. */ - virtual absl::optional congestionWindowInBytes() const PURE; + virtual std::optional congestionWindowInBytes() const PURE; /** - * @return the interface name for the socket, if the OS supports it. Otherwise, absl::nullopt. + * @return the interface name for the socket, if the OS supports it. Otherwise, std::nullopt. */ - virtual absl::optional interfaceName() PURE; + virtual std::optional interfaceName() PURE; }; using IoHandlePtr = std::unique_ptr; diff --git a/envoy/network/listen_socket.h b/envoy/network/listen_socket.h index c6f02d02403ba..7d36aebdef5e6 100644 --- a/envoy/network/listen_socket.h +++ b/envoy/network/listen_socket.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -13,7 +14,6 @@ #include "envoy/network/socket.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Network { @@ -79,11 +79,11 @@ class ConnectionSocket : public virtual Socket { virtual absl::string_view ja4Hash() const PURE; /** - * @return absl::optional An optional of the most recent round-trip + * @return std::optional An optional of the most recent round-trip * time of the connection. If the platform does not support this, then an empty optional is * returned. */ - virtual absl::optional lastRoundTripTime() PURE; + virtual std::optional lastRoundTripTime() PURE; /** * @return the current congestion window in bytes, or unset if not available or not @@ -91,7 +91,7 @@ class ConnectionSocket : public virtual Socket { * @note some congestion controller's cwnd is measured in number of packets, in that case the * return value is cwnd(in packets) times the connection's MSS. */ - virtual absl::optional congestionWindowInBytes() const PURE; + virtual std::optional congestionWindowInBytes() const PURE; /** * Dump debug state of the object in question to the provided ostream. diff --git a/envoy/network/listener.h b/envoy/network/listener.h index 87d92d22717b5..bdb0f59a59979 100644 --- a/envoy/network/listener.h +++ b/envoy/network/listener.h @@ -461,7 +461,7 @@ class UdpListenerCallbacks { virtual const IoHandle::UdpSaveCmsgConfig& udpSaveCmsgConfig() const PURE; }; -using UdpListenerCallbacksOptRef = absl::optional>; +using UdpListenerCallbacksOptRef = std::optional>; /** * An abstract socket listener. Free the listener to stop listening on the socket. diff --git a/envoy/network/proxy_protocol.h b/envoy/network/proxy_protocol.h index ca866e91a131f..cc4240ac3bf5b 100644 --- a/envoy/network/proxy_protocol.h +++ b/envoy/network/proxy_protocol.h @@ -1,13 +1,12 @@ #pragma once #include +#include #include #include #include "envoy/network/address.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Network { @@ -21,7 +20,7 @@ using ProxyProtocolTLVVector = std::vector; struct ProxyProtocolData { const Network::Address::InstanceConstSharedPtr src_addr_; const Network::Address::InstanceConstSharedPtr dst_addr_; - const ProxyProtocolTLVVector tlv_vector_{}; + const ProxyProtocolTLVVector tlv_vector_; std::string asStringForHash() const { return std::string(src_addr_ ? src_addr_->asString() : "null") + (dst_addr_ ? dst_addr_->asString() : "null"); @@ -31,7 +30,7 @@ struct ProxyProtocolData { enum class ProxyProtocolVersion { NotFound = 1, V1 = 2, V2 = 3 }; struct ProxyProtocolDataWithVersion : public ProxyProtocolData { - const absl::optional version_; + const std::optional version_; }; } // namespace Network } // namespace Envoy diff --git a/envoy/network/socket.h b/envoy/network/socket.h index 5cbd979aee599..1285c4fb7a516 100644 --- a/envoy/network/socket.h +++ b/envoy/network/socket.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -13,7 +14,6 @@ #include "envoy/ssl/connection.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Network { @@ -35,7 +35,7 @@ struct SocketOptionName { bool operator==(const SocketOptionName& rhs) const { return value_ == rhs.value_; } private: - absl::optional> value_; + std::optional> value_; }; // ENVOY_MAKE_SOCKET_OPTION_NAME is a helper macro to generate a @@ -256,13 +256,13 @@ class ConnectionInfoProvider { /** * @return Connection ID of the downstream connection, or unset if not available. **/ - virtual absl::optional connectionID() const PURE; + virtual std::optional connectionID() const PURE; /** * @return the name of the network interface used by local end of the connection, or unset if not *available. **/ - virtual absl::optional interfaceName() const PURE; + virtual std::optional interfaceName() const PURE; /** * Dumps the state of the ConnectionInfoProvider to the given ostream. @@ -291,7 +291,7 @@ class ConnectionInfoProvider { /** * @return roundTripTime of the connection */ - virtual const absl::optional& roundTripTime() const PURE; + virtual const std::optional& roundTripTime() const PURE; /** * @return the filter chain info provider backing this socket. @@ -440,9 +440,9 @@ class Socket { virtual Address::Type addressType() const PURE; /** - * @return the IP version used by the socket if address type is IP, absl::nullopt otherwise + * @return the IP version used by the socket if address type is IP, std::nullopt otherwise */ - virtual absl::optional ipVersion() const PURE; + virtual std::optional ipVersion() const PURE; /** * Close the underlying socket. @@ -544,7 +544,7 @@ class Socket { * @param state The state at which we would apply the options. * @return What we would apply to the socket at the provided state. Empty if we'd apply nothing. */ - virtual absl::optional
+ virtual std::optional
getOptionDetails(const Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const PURE; @@ -600,13 +600,13 @@ class Socket { * @return a ParentDrainedCallbackRegistrar for UDP listen sockets during hot restart. */ virtual OptRef parentDrainedCallbackRegistrar() const { - return absl::nullopt; + return std::nullopt; } }; using SocketPtr = std::unique_ptr; using SocketSharedPtr = std::shared_ptr; -using SocketOptRef = absl::optional>; +using SocketOptRef = std::optional>; } // namespace Network } // namespace Envoy diff --git a/envoy/network/transport_socket.h b/envoy/network/transport_socket.h index f79fe3566c340..670959aa924dc 100644 --- a/envoy/network/transport_socket.h +++ b/envoy/network/transport_socket.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/api/io_error.h" @@ -14,8 +15,6 @@ #include "envoy/ssl/context.h" #include "envoy/stream_info/filter_state.h" -#include "absl/types/optional.h" - #ifdef ENVOY_ENABLE_QUIC namespace quic { class QuicCryptoClientConfig; @@ -42,10 +41,10 @@ enum class ConnectionEvent; struct IoResult { IoResult(PostIoAction action, uint64_t bytes_processed, bool end_stream_read) : action_(action), bytes_processed_(bytes_processed), end_stream_read_(end_stream_read), - err_code_(absl::nullopt) {} + err_code_(std::nullopt) {} IoResult(PostIoAction action, uint64_t bytes_processed, bool end_stream_read, - absl::optional err_code) + std::optional err_code) : action_(action), bytes_processed_(bytes_processed), end_stream_read_(end_stream_read), err_code_(err_code) {} @@ -65,7 +64,7 @@ struct IoResult { /** * The underlying I/O error code. */ - absl::optional err_code_; + std::optional err_code_; }; /** @@ -161,8 +160,11 @@ class TransportSocket { /** * Closes the transport socket. * @param event supplies the connection event that is closing the socket. + * @param abort_reset if true, the connection is being torn down with a TCP RST and the + * transport socket should skip any graceful shutdown (e.g. TLS close_notify) so the + * peer reliably observes the reset rather than racing it against a clean close. */ - virtual void closeSocket(Network::ConnectionEvent event) PURE; + virtual void closeSocket(Network::ConnectionEvent event, bool abort_reset = false) PURE; /** * @param buffer supplies the buffer to read to. @@ -226,7 +228,7 @@ class TransportSocketOptions { * and should pass it through to the connection pool to ensure the correct endpoints are * selected and the upstream connection is set up accordingly. */ - virtual const absl::optional& serverNameOverride() const PURE; + virtual const std::optional& serverNameOverride() const PURE; /** * @return the optional overridden SAN names to verify, if the transport socket supports SAN @@ -258,7 +260,7 @@ class TransportSocketOptions { /** * @return optional PROXY protocol address information. */ - virtual absl::optional proxyProtocolOptions() const PURE; + virtual std::optional proxyProtocolOptions() const PURE; // Information for use by the http_11_proxy transport socket. struct Http11ProxyInfo { @@ -349,7 +351,7 @@ class UpstreamTransportSocketFactory : public virtual TransportSocketFactoryBase virtual Envoy::Ssl::ClientContextSharedPtr sslCtx() { return nullptr; } /* - * @return the ClientContextConfig, or absl::nullopt for non-TLS factories. + * @return the ClientContextConfig, or std::nullopt for non-TLS factories. */ virtual OptRef clientContextConfig() const { return {}; } diff --git a/envoy/network/udp_packet_writer_factory_factory.h b/envoy/network/udp_packet_writer_factory_factory.h new file mode 100644 index 0000000000000..e2ec4ab126d2e --- /dev/null +++ b/envoy/network/udp_packet_writer_factory_factory.h @@ -0,0 +1,35 @@ +#pragma once + +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/config/typed_config.h" +#include "envoy/network/udp_packet_writer_handler.h" +#include "envoy/server/factory_context.h" + +namespace Envoy { +namespace Network { + +/** + * UdpPacketWriterFactoryFactory adds an extra layer of indirection In order to + * support a UdpPacketWriterFactory whose behavior depends on the + * TypedConfig for that factory. The UdpPacketWriterFactoryFactory is created + * with a no-arg constructor based on the type of the config. Then this + * `createUdpPacketWriterFactory` can be called with the config to + * create an actual UdpPacketWriterFactory. + */ +class UdpPacketWriterFactoryFactory : public Envoy::Config::TypedFactory { +public: + ~UdpPacketWriterFactoryFactory() override = default; + + /** + * Creates an UdpPacketWriterFactory based on the specified config. + * @return the UdpPacketWriterFactory created. + */ + virtual UdpPacketWriterFactoryPtr + createUdpPacketWriterFactory(const envoy::config::core::v3::TypedExtensionConfig& config, + Server::Configuration::ListenerFactoryContext& context) PURE; + + std::string category() const override { return "envoy.udp_packet_writer"; } +}; + +} // namespace Network +} // namespace Envoy diff --git a/envoy/network/udp_packet_writer_handler.h b/envoy/network/udp_packet_writer_handler.h index 99cee531d6f35..e8b2042247d6a 100644 --- a/envoy/network/udp_packet_writer_handler.h +++ b/envoy/network/udp_packet_writer_handler.h @@ -125,27 +125,5 @@ class UdpPacketWriterFactory { using UdpPacketWriterFactoryPtr = std::unique_ptr; -/** - * UdpPacketWriterFactoryFactory adds an extra layer of indirection In order to - * support a UdpPacketWriterFactory whose behavior depends on the - * TypedConfig for that factory. The UdpPacketWriterFactoryFactory is created - * with a no-arg constructor based on the type of the config. Then this - * `createUdpPacketWriterFactory1 can be called with the config to - * create an actual UdpPacketWriterFactory. - */ -class UdpPacketWriterFactoryFactory : public Envoy::Config::TypedFactory { -public: - ~UdpPacketWriterFactoryFactory() override = default; - - /** - * Creates an UdpPacketWriterFactory based on the specified config. - * @return the UdpPacketWriterFactory created. - */ - virtual UdpPacketWriterFactoryPtr - createUdpPacketWriterFactory(const envoy::config::core::v3::TypedExtensionConfig& config) PURE; - - std::string category() const override { return "envoy.udp_packet_writer"; } -}; - } // namespace Network } // namespace Envoy diff --git a/envoy/protobuf/BUILD b/envoy/protobuf/BUILD index 95fd8d9ea1bc7..0b0c7688af79e 100644 --- a/envoy/protobuf/BUILD +++ b/envoy/protobuf/BUILD @@ -13,6 +13,7 @@ envoy_cc_library( hdrs = ["message_validator.h"], deps = [ "//envoy/common:exception_lib", + "//envoy/common:optref_lib", "//envoy/runtime:runtime_interface", "//source/common/protobuf", ], diff --git a/envoy/ratelimit/ratelimit.h b/envoy/ratelimit/ratelimit.h index 4e1dc34b57f1a..74b0118ad956b 100644 --- a/envoy/ratelimit/ratelimit.h +++ b/envoy/ratelimit/ratelimit.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -12,7 +13,6 @@ #include "envoy/type/v3/ratelimit_unit.pb.h" #include "absl/time/time.h" -#include "absl/types/optional.h" namespace Envoy { namespace RateLimit { @@ -54,8 +54,8 @@ using XRateLimitOption = RateLimitProto::XRateLimitOption; */ struct Descriptor { DescriptorEntries entries_; - absl::optional limit_ = absl::nullopt; - absl::optional hits_addend_ = absl::nullopt; + std::optional limit_ = std::nullopt; + std::optional hits_addend_ = std::nullopt; bool is_negative_hits_ = false; XRateLimitOption x_ratelimit_option_{}; diff --git a/envoy/rds/route_config_provider.h b/envoy/rds/route_config_provider.h index dd220bc99c10a..7a834a100fd12 100644 --- a/envoy/rds/route_config_provider.h +++ b/envoy/rds/route_config_provider.h @@ -1,14 +1,13 @@ #pragma once #include +#include #include "envoy/common/time.h" #include "envoy/rds/config.h" #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Rds { @@ -41,7 +40,7 @@ class RouteConfigProvider { * if the provider has not yet performed an initial configuration load, no information will be * returned. */ - virtual const absl::optional& configInfo() const PURE; + virtual const std::optional& configInfo() const PURE; /** * @return the last time this RouteConfigProvider was updated. Used for config dumps. diff --git a/envoy/rds/route_config_update_receiver.h b/envoy/rds/route_config_update_receiver.h index c4fd2792dc658..9811bec7ac4e8 100644 --- a/envoy/rds/route_config_update_receiver.h +++ b/envoy/rds/route_config_update_receiver.h @@ -1,13 +1,12 @@ #pragma once #include +#include #include "envoy/common/pure.h" #include "envoy/common/time.h" #include "envoy/rds/route_config_provider.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Rds { @@ -34,11 +33,11 @@ class RouteConfigUpdateReceiver { virtual uint64_t configHash() const PURE; /** - * @return absl::optional containing an instance of + * @return std::optional containing an instance of * RouteConfigProvider::ConfigInfo if RouteConfiguration has been updated at least once. Otherwise - * returns an empty absl::optional. + * returns an empty std::optional. */ - virtual const absl::optional& configInfo() const PURE; + virtual const std::optional& configInfo() const PURE; /** * @return Protobuf::Message& current RouteConfiguration. diff --git a/envoy/registry/registry.h b/envoy/registry/registry.h index b13aef7c1c56c..8ef45969efd3a 100644 --- a/envoy/registry/registry.h +++ b/envoy/registry/registry.h @@ -37,7 +37,7 @@ class FactoryRegistryProxy { virtual std::vector registeredNames() const PURE; // Return all registered factory names, including disabled factories. virtual std::vector allRegisteredNames() const PURE; - virtual absl::optional + virtual std::optional getFactoryVersion(absl::string_view name) const PURE; virtual bool disableFactory(absl::string_view) PURE; virtual bool isFactoryDisabled(absl::string_view) const PURE; @@ -57,7 +57,7 @@ template class FactoryRegistryProxyImpl : public FactoryRegistryPro return FactoryRegistry::registeredNames(true); } - absl::optional + std::optional getFactoryVersion(absl::string_view name) const override { return FactoryRegistry::getFactoryVersion(name); } @@ -328,11 +328,11 @@ template class FactoryRegistry : public Logger::Loggable + static std::optional getFactoryVersion(absl::string_view name) { auto it = versionedFactories().find(name); if (it == versionedFactories().end()) { - return absl::nullopt; + return std::nullopt; } return it->second; } diff --git a/envoy/router/BUILD b/envoy/router/BUILD index cf351eb7f73a8..55c51960c79e1 100644 --- a/envoy/router/BUILD +++ b/envoy/router/BUILD @@ -53,7 +53,6 @@ envoy_cc_library( ":rds_interface", "//envoy/common:time_interface", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", ], @@ -69,7 +68,6 @@ envoy_cc_library( "//envoy/stream_info:stream_info_interface", "//envoy/upstream:cluster_manager_interface", "//envoy/upstream:host_description_interface", - "@abseil-cpp//absl/types:optional", ], ) @@ -99,7 +97,6 @@ envoy_cc_library( "//envoy/upstream:retry_interface", "//source/common/protobuf", "//source/common/protobuf:utility_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", @@ -140,7 +137,6 @@ envoy_cc_library( hdrs = ["string_accessor.h"], deps = [ "//envoy/stream_info:filter_state_interface", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/router/route_config_update_receiver.h b/envoy/router/route_config_update_receiver.h index 93619ea87e6be..48a4f46242e8a 100644 --- a/envoy/router/route_config_update_receiver.h +++ b/envoy/router/route_config_update_receiver.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/common/pure.h" #include "envoy/common/time.h" @@ -10,8 +11,6 @@ #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Router { @@ -56,7 +55,7 @@ class RouteConfigUpdateReceiver : public Rds::RouteConfigUpdateReceiver { * @return the union of all resource names and aliases (if any) received with the last VHDS * update. */ - virtual const std::set& resourceIdsInLastVhdsUpdate() PURE; + virtual const std::set& resourceIdsInLastVhdsUpdate() const PURE; }; using RouteConfigUpdatePtr = std::unique_ptr; diff --git a/envoy/router/router.h b/envoy/router/router.h index 306474c4489be..6c9bd2b490100 100644 --- a/envoy/router/router.h +++ b/envoy/router/router.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "envoy/access_log/access_log.h" @@ -32,8 +33,6 @@ #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Formatter { class Formatter; @@ -96,7 +95,8 @@ class DirectResponseEntry : public ResponseEntry { * @return std::string the redirect URL if this DirectResponseEntry is a redirect, * or an empty string otherwise. */ - virtual std::string newUri(const Http::RequestHeaderMap& headers) const PURE; + virtual std::string newUri(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info) const PURE; /** * Format the response body for direct responses. Users should pass @@ -175,14 +175,14 @@ class CorsPolicy : public RouteSpecificFilterConfig { virtual const std::string& maxAge() const PURE; /** - * @return const absl::optional& Whether access-control-allow-credentials should be true. + * @return const std::optional& Whether access-control-allow-credentials should be true. */ - virtual const absl::optional& allowCredentials() const PURE; + virtual const std::optional& allowCredentials() const PURE; /** - * @return const absl::optional& How to handle access-control-request-private-network. + * @return const std::optional& How to handle access-control-request-private-network. */ - virtual const absl::optional& allowPrivateNetworkAccess() const PURE; + virtual const std::optional& allowPrivateNetworkAccess() const PURE; /** * @return bool Whether CORS is enabled for the route or virtual host. @@ -198,7 +198,7 @@ class CorsPolicy : public RouteSpecificFilterConfig { * @return bool whether preflight requests with origin not matching * configured allowed origins should be forwarded upstream. */ - virtual const absl::optional& forwardNotMatchingPreflights() const PURE; + virtual const std::optional& forwardNotMatchingPreflights() const PURE; }; /** @@ -212,7 +212,7 @@ class ResetHeaderParser { * Iterate over the headers, choose the first one that matches by name, and try to parse its * value. */ - virtual absl::optional + virtual std::optional parseInterval(TimeSource& time_source, const Http::HeaderMap& headers) const PURE; }; @@ -312,14 +312,14 @@ class RetryPolicy { virtual const std::vector& retriableRequestHeaders() const PURE; /** - * @return absl::optional base retry interval + * @return std::optional base retry interval */ - virtual absl::optional baseInterval() const PURE; + virtual std::optional baseInterval() const PURE; /** - * @return absl::optional maximum retry interval + * @return std::optional maximum retry interval */ - virtual absl::optional maxInterval() const PURE; + virtual std::optional maxInterval() const PURE; /** * @return std::vector& list of reset header @@ -332,6 +332,11 @@ class RetryPolicy { * back-off interval parsed from response headers. */ virtual std::chrono::milliseconds resetMaxInterval() const PURE; + + /** + * @return whether the route-selected upstream cluster should be refreshed before a retry attempt. + */ + virtual bool refreshClusterOnRetry() const PURE; }; /** @@ -439,7 +444,7 @@ class RetryState { * interval directly, or in the form of a unix timestamp relative to the current system time. * @return the interval if parsing was successful. */ - virtual absl::optional + virtual std::optional parseResetInterval(const Http::ResponseHeaderMap& response_headers) const PURE; /** @@ -585,7 +590,7 @@ class ShadowPolicy { /** * @return true if the trace span should be sampled. */ - virtual absl::optional traceSampled() const PURE; + virtual std::optional traceSampled() const PURE; /** * @return true if host name should be suffixed with "-shadow". @@ -663,7 +668,7 @@ class VirtualCluster { /** * @return the string name of the virtual cluster. */ - virtual const absl::optional& name() const PURE; + virtual const std::optional& name() const PURE; /** * @return the stat-name of the virtual cluster. @@ -858,13 +863,13 @@ class TlsContextMatchCriteria { /** * @return bool indicating whether the client presented credentials. */ - virtual const absl::optional& presented() const PURE; + virtual const std::optional& presented() const PURE; /** * @return bool indicating whether the client credentials successfully validated against the TLS * context validation context. */ - virtual const absl::optional& validated() const PURE; + virtual const std::optional& validated() const PURE; }; using TlsContextMatchCriteriaConstPtr = std::unique_ptr; @@ -1077,13 +1082,13 @@ class RouteEntry : public ResponseEntry { * @return optional the route's idle timeout. Zero indicates a * disabled idle timeout, while nullopt indicates deference to the global timeout. */ - virtual absl::optional idleTimeout() const PURE; + virtual std::optional idleTimeout() const PURE; /** * @return optional the route's flush timeout. Zero indicates a * disabled idle timeout, while nullopt indicates deference to the global timeout. */ - virtual absl::optional flushTimeout() const PURE; + virtual std::optional flushTimeout() const PURE; /** * @return true if new style max_stream_duration config should be used over the old style. @@ -1093,32 +1098,32 @@ class RouteEntry : public ResponseEntry { /** * @return optional the route's maximum stream duration. */ - virtual absl::optional maxStreamDuration() const PURE; + virtual std::optional maxStreamDuration() const PURE; /** * @return optional the max grpc-timeout this route will allow. */ - virtual absl::optional grpcTimeoutHeaderMax() const PURE; + virtual std::optional grpcTimeoutHeaderMax() const PURE; /** * @return optional the delta between grpc-timeout and enforced grpc * timeout. */ - virtual absl::optional grpcTimeoutHeaderOffset() const PURE; + virtual std::optional grpcTimeoutHeaderOffset() const PURE; /** - * @return absl::optional the maximum allowed timeout value derived + * @return std::optional the maximum allowed timeout value derived * from 'grpc-timeout' header of a gRPC request. Non-present value disables use of 'grpc-timeout' * header, while 0 represents infinity. */ - virtual absl::optional maxGrpcTimeout() const PURE; + virtual std::optional maxGrpcTimeout() const PURE; /** - * @return absl::optional the timeout offset to apply to the timeout + * @return std::optional the timeout offset to apply to the timeout * provided by the 'grpc-timeout' header of a gRPC request. This value will be positive and should * be subtracted from the value provided by the header. */ - virtual absl::optional grpcTimeoutOffset() const PURE; + virtual std::optional grpcTimeoutOffset() const PURE; /** * @return bool true if the :authority header should be overwritten with the upstream hostname. @@ -1311,7 +1316,7 @@ class Route { * @return true if the filter is disabled for this route, false if the filter is enabled. * nullopt if no decision can be made explicitly for the filter. */ - virtual absl::optional filterDisabled(absl::string_view config_name) const PURE; + virtual std::optional filterDisabled(absl::string_view config_name) const PURE; /** * This is a helper to get the route's per-filter config if it exists, up along the config @@ -1546,6 +1551,8 @@ class GenericConnPool { virtual bool valid() const PURE; }; +using GenericConnPoolPtr = std::unique_ptr; + /** * An API for the interactions the upstream stream needs to have with the downstream stream * and/or router components @@ -1602,7 +1609,7 @@ class GenericConnectionPoolCallbacks { Upstream::HostDescriptionConstSharedPtr host, const Network::ConnectionInfoProvider& connection_info_provider, StreamInfo::StreamInfo& info, - absl::optional protocol) PURE; + std::optional protocol) PURE; // @return the UpstreamToDownstream interface for this stream. // @@ -1674,8 +1681,6 @@ class GenericUpstream { virtual const StreamInfo::BytesMeterSharedPtr& bytesMeter() PURE; }; -using GenericConnPoolPtr = std::unique_ptr; - /* * A factory for creating generic connection pools. */ @@ -1695,7 +1700,7 @@ class GenericConnPoolFactory : public Envoy::Config::TypedFactory { virtual GenericConnPoolPtr createGenericConnPool( Upstream::HostConstSharedPtr host, Upstream::ThreadLocalCluster& thread_local_cluster, GenericConnPoolFactory::UpstreamProtocol upstream_protocol, - Upstream::ResourcePriority priority, absl::optional downstream_protocol, + Upstream::ResourcePriority priority, std::optional downstream_protocol, Upstream::LoadBalancerContext* ctx, const Protobuf::Message& config) const PURE; }; diff --git a/envoy/router/router_filter_interface.h b/envoy/router/router_filter_interface.h index f0cb02eddb7d8..47607667af269 100644 --- a/envoy/router/router_filter_interface.h +++ b/envoy/router/router_filter_interface.h @@ -138,7 +138,7 @@ class RouterFilterInterface { /* * @returns the dynamic max stream duraration for this stream, if set. */ - virtual absl::optional dynamicMaxStreamDuration() const PURE; + virtual std::optional dynamicMaxStreamDuration() const PURE; /* * @returns the request headers for the stream. diff --git a/envoy/router/scopes.h b/envoy/router/scopes.h index 47ba039756eb1..936b72b76fd48 100644 --- a/envoy/router/scopes.h +++ b/envoy/router/scopes.h @@ -69,13 +69,11 @@ using ScopeKeyPtr = std::unique_ptr; // String fragment. class StringKeyFragment : public ScopeKeyFragmentBase { public: - explicit StringKeyFragment(absl::string_view value) - : value_(value), hash_(HashUtil::xxHash64(value_)) {} + explicit StringKeyFragment(absl::string_view value) : hash_(HashUtil::xxHash64(value)) {} uint64_t hash() const override { return hash_; } private: - const std::string value_; const uint64_t hash_; }; diff --git a/envoy/runtime/BUILD b/envoy/runtime/BUILD index a5630f3f3fc29..a5c9ecff5ccbd 100644 --- a/envoy/runtime/BUILD +++ b/envoy/runtime/BUILD @@ -12,12 +12,11 @@ envoy_cc_library( name = "runtime_interface", hdrs = ["runtime.h"], deps = [ - "//envoy/stats:stats_interface", + "//envoy/stats:scope_interface", "//envoy/thread_local:thread_local_object", "//source/common/common:assert_lib", "//source/common/singleton:threadsafe_singleton", "@abseil-cpp//absl/container:node_hash_map", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/type/v3:pkg_cc_proto", ], ) diff --git a/envoy/runtime/runtime.h b/envoy/runtime/runtime.h index c7ae8fcfd3a8c..1ffa0bab3d5e7 100644 --- a/envoy/runtime/runtime.h +++ b/envoy/runtime/runtime.h @@ -4,11 +4,12 @@ #include #include #include +#include #include #include #include "envoy/common/pure.h" -#include "envoy/stats/store.h" +#include "envoy/stats/scope.h" #include "envoy/thread_local/thread_local_object.h" #include "envoy/type/v3/percent.pb.h" @@ -17,7 +18,6 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/node_hash_map.h" -#include "absl/types/optional.h" namespace Envoy { @@ -34,10 +34,10 @@ class Snapshot : public ThreadLocal::ThreadLocalObject { public: struct Entry { std::string raw_string_value_; - absl::optional uint_value_; - absl::optional double_value_; - absl::optional fractional_percent_value_; - absl::optional bool_value_; + std::optional uint_value_; + std::optional double_value_; + std::optional fractional_percent_value_; + std::optional bool_value_; }; using EntryMap = absl::flat_hash_map; @@ -165,11 +165,11 @@ class Snapshot : public ThreadLocal::ThreadLocalObject { const envoy::type::v3::FractionalPercent& default_value, uint64_t random_value) const PURE; - using ConstStringOptRef = absl::optional>; + using ConstStringOptRef = std::optional>; /** * Fetch raw runtime data based on key. * @param key supplies the key to fetch. - * @return absl::nullopt if the key does not exist or reference to the value std::string. + * @return std::nullopt if the key does not exist or reference to the value std::string. */ virtual ConstStringOptRef get(absl::string_view key) const PURE; @@ -231,6 +231,14 @@ class Loader { */ virtual absl::Status initialize(Upstream::ClusterManager& cm) PURE; + /** + * Called from server initialization once worker dispatchers are registered with ThreadLocal + * (ListenerManager construction), before those worker threads start running their event loops. + * Implementations may use this for any setup that depends on worker threads being registered + * (e.g. publishing thread-local state to all registered dispatchers). + */ + virtual absl::Status onWorkerThreadsRegistered() PURE; + /** * @return const Snapshot& the current snapshot. This reference is safe to use for the duration of * the calling routine, but may be overwritten on a future event loop cycle so should be diff --git a/envoy/secret/BUILD b/envoy/secret/BUILD index 0f510ec3992e6..f3d7c6b2a179f 100644 --- a/envoy/secret/BUILD +++ b/envoy/secret/BUILD @@ -11,6 +11,9 @@ envoy_package() envoy_cc_library( name = "secret_callbacks_interface", hdrs = ["secret_callbacks.h"], + deps = [ + "//envoy/common:pure_lib", + ], ) envoy_cc_library( @@ -30,7 +33,10 @@ envoy_cc_library( hdrs = ["secret_manager.h"], deps = [ ":secret_provider_interface", - "//envoy/init:target_interface", + "//envoy/common:optref_lib", + "//envoy/common:pure_lib", + "//envoy/init:manager_interface", + "@abseil-cpp//absl/status", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", ], diff --git a/envoy/secret/secret_manager.h b/envoy/secret/secret_manager.h index 75276e023f9fa..98cddf5df5a41 100644 --- a/envoy/secret/secret_manager.h +++ b/envoy/secret/secret_manager.h @@ -1,11 +1,17 @@ #pragma once +#include #include +#include "envoy/common/optref.h" +#include "envoy/common/pure.h" #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" +#include "envoy/init/manager.h" #include "envoy/secret/secret_provider.h" +#include "absl/status/status.h" + namespace Envoy { namespace Server { diff --git a/envoy/server/BUILD b/envoy/server/BUILD index 1a732fc523ac9..fe73ba5dc6a3b 100644 --- a/envoy/server/BUILD +++ b/envoy/server/BUILD @@ -13,11 +13,18 @@ envoy_cc_library( hdrs = ["admin.h"], deps = [ ":config_tracker_interface", + "//envoy/access_log:access_log_interface", "//envoy/buffer:buffer_interface", + "//envoy/common:pure_lib", + "//envoy/http:codec_interface", + "//envoy/http:codes_interface", "//envoy/http:filter_interface", "//envoy/http:header_map_interface", "//envoy/http:query_params_interface", - "//envoy/network:listen_socket_interface", + "//envoy/network:address_interface", + "//envoy/network:connection_handler_interface", + "//envoy/network:socket_interface", + "@abseil-cpp//absl/strings", ], ) @@ -38,7 +45,6 @@ envoy_cc_library( "//envoy/http:context_interface", "//envoy/stats:sink_interface", "//envoy/upstream:cluster_manager_interface", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", ], ) @@ -102,8 +108,16 @@ envoy_cc_library( name = "hot_restart_interface", hdrs = ["hot_restart.h"], deps = [ + "//envoy/common:exception_lib", + "//envoy/common:optref_lib", + "//envoy/common:pure_lib", "//envoy/event:dispatcher_interface", + "//envoy/network:address_interface", + "//envoy/network:listener_interface", + "//envoy/network:socket_interface", + "//envoy/stats:stats_interface", "//envoy/thread:thread_interface", + "@abseil-cpp//absl/strings", ], ) @@ -114,25 +128,36 @@ envoy_cc_library( ":admin_interface", ":configuration_interface", ":drain_manager_interface", + ":factory_context_interface", ":hot_restart_interface", ":lifecycle_notifier_interface", ":listener_manager_interface", ":options_interface", + ":process_context_interface", "//envoy/access_log:access_log_interface", "//envoy/api:api_interface", "//envoy/common:mutex_tracer", + "//envoy/common:optref_lib", + "//envoy/common:pure_lib", + "//envoy/common:time_interface", "//envoy/config:xds_manager_interface", "//envoy/event:timer_interface", "//envoy/http:context_interface", "//envoy/http:query_params_interface", "//envoy/init:manager_interface", "//envoy/local_info:local_info_interface", + "//envoy/network:connection_handler_interface", + "//envoy/network:dns_interface", + "//envoy/protobuf:message_validator_interface", + "//envoy/router:context_interface", "//envoy/runtime:runtime_interface", "//envoy/secret:secret_manager_interface", "//envoy/server/overload:overload_manager_interface", + "//envoy/singleton:manager_interface", "//envoy/ssl:context_manager_interface", + "//envoy/ssl/private_key:private_key_interface", + "//envoy/stats:stats_interface", "//envoy/thread_local:thread_local_interface", - "//envoy/tracing:tracer_interface", "//envoy/upstream:cluster_manager_interface", "@envoy_api//envoy/config/trace/v3:pkg_cc_proto", ], @@ -153,9 +178,16 @@ envoy_cc_library( name = "worker_interface", hdrs = ["worker.h"], deps = [ + "//envoy/common:optref_lib", + "//envoy/common:pure_lib", + "//envoy/common:random_generator_interface", + "//envoy/network:connection_handler_interface", + "//envoy/network:filter_interface", + "//envoy/network:listener_interface", "//envoy/runtime:runtime_interface", "//envoy/server:guarddog_interface", "//envoy/server/overload:overload_manager_interface", + "//envoy/stats:stats_interface", ], ) @@ -245,14 +277,18 @@ envoy_cc_library( deps = [ ":api_listener_interface", ":drain_manager_interface", - ":filter_config_interface", + ":factory_context_interface", ":guarddog_interface", + "//envoy/common:pure_lib", "//envoy/filter:config_provider_manager_interface", + "//envoy/network:address_interface", + "//envoy/network:connection_handler_interface", "//envoy/network:filter_interface", - "//envoy/network:listen_socket_interface", + "//envoy/network:socket_interface", "//envoy/network:socket_interface_interface", - "//envoy/ssl:context_interface", "//source/common/protobuf", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", "@envoy_api//envoy/admin/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", diff --git a/envoy/server/admin.h b/envoy/server/admin.h index a0b4fa4934584..11ca15834df85 100644 --- a/envoy/server/admin.h +++ b/envoy/server/admin.h @@ -1,15 +1,22 @@ #pragma once +#include #include +#include #include +#include +#include "envoy/access_log/access_log.h" #include "envoy/buffer/buffer.h" #include "envoy/common/pure.h" +#include "envoy/http/codec.h" #include "envoy/http/codes.h" #include "envoy/http/filter.h" #include "envoy/http/header_map.h" #include "envoy/http/query_params.h" -#include "envoy/network/listen_socket.h" +#include "envoy/network/address.h" +#include "envoy/network/connection_handler.h" +#include "envoy/network/socket.h" #include "envoy/server/config_tracker.h" #include "absl/strings/string_view.h" @@ -52,7 +59,7 @@ class AdminStream { /** * Return the HTTP/1 stream encoder options if applicable. If the stream is not HTTP/1 returns - * absl::nullopt. + * std::nullopt. */ virtual Http::Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() PURE; @@ -111,7 +118,7 @@ class Admin { Type type_; std::string id_; // HTML form ID and query-param name (JS var name rules). std::string help_; // Rendered into home-page HTML and /help text. - std::vector enum_choices_{}; + std::vector enum_choices_; }; using ParamDescriptorVec = std::vector; @@ -163,7 +170,7 @@ class Admin { const GenRequestFn handler_; const bool removable_; const bool mutates_server_state_; - const ParamDescriptorVec params_{}; + const ParamDescriptorVec params_; }; /** diff --git a/envoy/server/api_listener.h b/envoy/server/api_listener.h index a32f73dbc2035..15f84cfc513fd 100644 --- a/envoy/server/api_listener.h +++ b/envoy/server/api_listener.h @@ -43,7 +43,7 @@ class ApiListener { }; using ApiListenerPtr = std::unique_ptr; -using ApiListenerOptRef = absl::optional>; +using ApiListenerOptRef = std::optional>; class ApiListenerFactory : public Config::UntypedFactory { public: diff --git a/envoy/server/configuration.h b/envoy/server/configuration.h index 9da04d1ac7012..c72060a1f73ee 100644 --- a/envoy/server/configuration.h +++ b/envoy/server/configuration.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,8 +12,6 @@ #include "envoy/stats/sink.h" #include "envoy/upstream/cluster_manager.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Server { namespace Configuration { @@ -178,9 +177,9 @@ class Initial { virtual Admin& admin() PURE; /** - * @return absl::optional the path to look for flag files. + * @return std::optional the path to look for flag files. */ - virtual absl::optional flagsPath() const PURE; + virtual std::optional flagsPath() const PURE; /** * @return const envoy::config::bootstrap::v2::LayeredRuntime& runtime diff --git a/envoy/server/factory_context.h b/envoy/server/factory_context.h index ee9fa05618884..e9fcd320fed48 100644 --- a/envoy/server/factory_context.h +++ b/envoy/server/factory_context.h @@ -291,11 +291,6 @@ class FactoryContext : public virtual GenericFactoryContext { public: ~FactoryContext() override = default; - /** - * @return Stats::Scope& the listener's stats scope. - */ - virtual Stats::Scope& listenerScope() PURE; - /** * @return const Network::DrainDecision& a drain decision that filters can use to determine if * they should be doing graceful closes on connections when possible. @@ -303,9 +298,25 @@ class FactoryContext : public virtual GenericFactoryContext { virtual const Network::DrainDecision& drainDecision() PURE; /** - * @return ListenerInfo description of the listener. + * @return envoy::config::core::v3::TrafficDirection the direction of the + * downstream traffic relative to the local proxy. */ - virtual const Network::ListenerInfo& listenerInfo() const PURE; + virtual envoy::config::core::v3::TrafficDirection direction() const PURE; + + /** + * @return whether the filter is applied to a Quic listener. + */ + virtual bool isQuic() const PURE; + + /** + * @return bool whether the filter should bypass overload manager actions. + */ + virtual bool shouldBypassOverloadManager() const PURE; + + /** + * @return Stats::Scope& same as scope() but with the specific parent prefix. + */ + virtual Stats::Scope& prefixedScope() PURE; }; /** @@ -341,7 +352,18 @@ class FilterChainBaseAction : public Matcher::Action { * An implementation of FactoryContext. The life time should cover the lifetime of the filter chains * and connections. It can be used to create ListenerFilterChain. */ -class ListenerFactoryContext : public virtual FactoryContext {}; +class ListenerFactoryContext : public virtual FactoryContext { +public: + /** + * @return Stats::Scope& the listener's stats scope. + */ + virtual Stats::Scope& listenerScope() PURE; + + /** + * @return ListenerInfo description of the listener. + */ + virtual const Network::ListenerInfo& listenerInfo() const PURE; +}; /** * FactoryContext for ProtocolOptionsFactory. diff --git a/envoy/server/hot_restart.h b/envoy/server/hot_restart.h index c038f1f871950..a37fbb166523e 100644 --- a/envoy/server/hot_restart.h +++ b/envoy/server/hot_restart.h @@ -1,13 +1,23 @@ #pragma once #include +#include +#include +#include #include +#include "envoy/common/exception.h" +#include "envoy/common/optref.h" #include "envoy/common/pure.h" #include "envoy/event/dispatcher.h" +#include "envoy/network/address.h" +#include "envoy/network/listener.h" +#include "envoy/network/socket.h" #include "envoy/stats/store.h" #include "envoy/thread/thread.h" +#include "absl/strings/string_view.h" + namespace Envoy { namespace Server { @@ -86,7 +96,7 @@ class HotRestart { * to start up in the new process. * @return response if the parent is alive. */ - virtual absl::optional sendParentAdminShutdownRequest() PURE; + virtual std::optional sendParentAdminShutdownRequest() PURE; /** * Tell our parent process to gracefully terminate itself. diff --git a/envoy/server/instance.h b/envoy/server/instance.h index 0d82f00dcbe55..a45ba093f488e 100644 --- a/envoy/server/instance.h +++ b/envoy/server/instance.h @@ -1,14 +1,15 @@ #pragma once #include -#include +#include #include -#include #include "envoy/access_log/access_log.h" #include "envoy/api/api.h" #include "envoy/common/mutex_tracer.h" -#include "envoy/common/random_generator.h" +#include "envoy/common/optref.h" +#include "envoy/common/pure.h" +#include "envoy/common/time.h" #include "envoy/config/trace/v3/http_tracer.pb.h" #include "envoy/config/xds_manager.h" #include "envoy/event/timer.h" @@ -17,20 +18,27 @@ #include "envoy/http/http_server_properties_cache.h" #include "envoy/init/manager.h" #include "envoy/local_info/local_info.h" -#include "envoy/network/listen_socket.h" +#include "envoy/network/connection_handler.h" +#include "envoy/network/dns.h" +#include "envoy/protobuf/message_validator.h" +#include "envoy/router/context.h" #include "envoy/runtime/runtime.h" #include "envoy/secret/secret_manager.h" #include "envoy/server/admin.h" #include "envoy/server/configuration.h" #include "envoy/server/drain_manager.h" +#include "envoy/server/factory_context.h" #include "envoy/server/hot_restart.h" #include "envoy/server/lifecycle_notifier.h" #include "envoy/server/listener_manager.h" #include "envoy/server/options.h" #include "envoy/server/overload/overload_manager.h" +#include "envoy/server/process_context.h" +#include "envoy/singleton/manager.h" #include "envoy/ssl/context_manager.h" +#include "envoy/ssl/private_key/private_key.h" +#include "envoy/stats/store.h" #include "envoy/thread_local/thread_local.h" -#include "envoy/tracing/tracer.h" #include "envoy/upstream/cluster_manager.h" namespace Envoy { @@ -99,7 +107,7 @@ class Instance { * @param options - if provided, options are passed through to shutdownListener. */ virtual void - drainListeners(OptRef options = absl::nullopt) PURE; + drainListeners(OptRef options = std::nullopt) PURE; /** * @return DrainManager& singleton for use by the entire server. diff --git a/envoy/server/listener_manager.h b/envoy/server/listener_manager.h index 161d2f5196d08..0eff8b0c84ab0 100644 --- a/envoy/server/listener_manager.h +++ b/envoy/server/listener_manager.h @@ -1,23 +1,33 @@ #pragma once +#include +#include +#include +#include #include #include "envoy/admin/v3/config_dump.pb.h" +#include "envoy/common/pure.h" #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/config/listener/v3/listener.pb.h" #include "envoy/config/listener/v3/listener_components.pb.h" #include "envoy/filter/config_provider_manager.h" +#include "envoy/network/address.h" +#include "envoy/network/connection_handler.h" #include "envoy/network/filter.h" -#include "envoy/network/listen_socket.h" #include "envoy/network/listener.h" +#include "envoy/network/socket.h" #include "envoy/network/socket_interface.h" #include "envoy/server/api_listener.h" #include "envoy/server/drain_manager.h" -#include "envoy/server/filter_config.h" +#include "envoy/server/factory_context.h" #include "envoy/server/guarddog.h" #include "source/common/protobuf/protobuf.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" + namespace Envoy { namespace Filter { class TcpListenerFilterConfigProviderManagerImpl; @@ -302,6 +312,12 @@ class ListenerManager { */ virtual ApiListenerOptRef apiListener() PURE; + /** + * @return the server's API Listener by name if it exists, nullopt if it does + * not. + */ + virtual ApiListenerOptRef apiListener(absl::string_view) { return apiListener(); } + /* * @return TRUE if the worker has started or FALSE if not. */ @@ -323,6 +339,8 @@ class ListenerManager { // combination of flags, such as listeners(ListenerState::WARMING|ListenerState::ACTIVE) constexpr ListenerManager::ListenerState operator|(const ListenerManager::ListenerState lhs, const ListenerManager::ListenerState rhs) { + // Bitmask combinations intentionally produce intermediate values that are not named enumerators. + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) return static_cast(static_cast(lhs) | static_cast(rhs)); } diff --git a/envoy/server/options.h b/envoy/server/options.h index 170838bacf903..7138ddf7e6a59 100644 --- a/envoy/server/options.h +++ b/envoy/server/options.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/admin/v3/server_info.pb.h" @@ -10,7 +11,6 @@ #include "envoy/network/address.h" #include "envoy/stats/tag.h" -#include "absl/types/optional.h" #include "spdlog/spdlog.h" namespace Envoy { diff --git a/envoy/server/overload/BUILD b/envoy/server/overload/BUILD index e300779d69793..79fee2cd8c322 100644 --- a/envoy/server/overload/BUILD +++ b/envoy/server/overload/BUILD @@ -17,7 +17,6 @@ envoy_cc_library( "//envoy/event:dispatcher_interface", "//envoy/thread_local:thread_local_interface", "//source/common/singleton:const_singleton", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/overload/v3:pkg_cc_proto", ], ) diff --git a/envoy/server/overload/overload_manager.h b/envoy/server/overload/overload_manager.h index 7e1aaa30eda8f..f6e5bc8e809c6 100644 --- a/envoy/server/overload/overload_manager.h +++ b/envoy/server/overload/overload_manager.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/common/pure.h" @@ -11,8 +12,6 @@ #include "source/common/singleton/const_singleton.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Server { /** @@ -42,16 +41,20 @@ class OverloadActionNameValues { // Overload action to reset streams using excessive memory. const std::string ResetStreams = "envoy.overload_actions.reset_high_memory_stream"; + // Overload action to terminate idle downstream HTTP connections. + const std::string CloseIdleHttpConnections = "envoy.overload_actions.close_idle_http_connections"; + // This should be kept current with the Overload actions available. // This is the last member of this class to duplicating the strings with // proper lifetime guarantees. - const std::array WellKnownActions = {StopAcceptingRequests, + const std::array WellKnownActions = {StopAcceptingRequests, DisableHttpKeepAlive, StopAcceptingConnections, RejectIncomingConnections, ShrinkHeap, ReduceTimeouts, - ResetStreams}; + ResetStreams, + CloseIdleHttpConnections}; }; using OverloadActionNames = ConstSingleton; @@ -114,7 +117,7 @@ class OverloadManager : public LoadShedPointProvider { * Get the configuration for the ShrinkHeap overload action. * @return optional config, empty if no ShrinkHeap action is configured or has no typed_config. */ - virtual absl::optional + virtual std::optional getShrinkHeapConfig() const PURE; }; diff --git a/envoy/server/overload/thread_local_overload_state.h b/envoy/server/overload/thread_local_overload_state.h index 7bf014ec29776..fef659053ff63 100644 --- a/envoy/server/overload/thread_local_overload_state.h +++ b/envoy/server/overload/thread_local_overload_state.h @@ -49,6 +49,12 @@ using OverloadProactiveResources = ConstSingleton -#include "absl/types/optional.h" +#include "envoy/common/pure.h" namespace Envoy { @@ -14,7 +14,7 @@ class ProcessObject { virtual ~ProcessObject() = default; }; -using ProcessObjectOptRef = absl::optional>; +using ProcessObjectOptRef = std::optional>; /** * Context passed to filters to access resources from non-Envoy parts of the @@ -30,6 +30,6 @@ class ProcessContext { virtual ProcessObject& get() const PURE; }; -using ProcessContextOptRef = absl::optional>; +using ProcessContextOptRef = std::optional>; } // namespace Envoy diff --git a/envoy/server/resource_monitor_config.h b/envoy/server/resource_monitor_config.h index a7d9086c28f0d..7fa1ee705d433 100644 --- a/envoy/server/resource_monitor_config.h +++ b/envoy/server/resource_monitor_config.h @@ -5,6 +5,7 @@ #include "envoy/config/typed_config.h" #include "envoy/event/dispatcher.h" #include "envoy/protobuf/message_validator.h" +#include "envoy/runtime/runtime.h" #include "envoy/server/options.h" #include "envoy/server/proactive_resource_monitor.h" #include "envoy/server/resource_monitor.h" @@ -40,6 +41,11 @@ class ResourceMonitorFactoryContext { * messages. */ virtual ProtobufMessage::ValidationVisitor& messageValidationVisitor() PURE; + + /** + * @return Runtime::Loader& the runtime loader for runtime key overrides. + */ + virtual Runtime::Loader& runtime() PURE; }; /** diff --git a/envoy/server/worker.h b/envoy/server/worker.h index baf60c78496d1..279a7a3760754 100644 --- a/envoy/server/worker.h +++ b/envoy/server/worker.h @@ -1,11 +1,22 @@ #pragma once +#include #include - -#include "envoy/event/dispatcher.h" +#include +#include +#include +#include + +#include "envoy/common/optref.h" +#include "envoy/common/pure.h" +#include "envoy/common/random_generator.h" +#include "envoy/network/connection_handler.h" +#include "envoy/network/filter.h" +#include "envoy/network/listener.h" #include "envoy/runtime/runtime.h" #include "envoy/server/guarddog.h" #include "envoy/server/overload/overload_manager.h" +#include "envoy/stats/scope.h" namespace Envoy { namespace Server { @@ -34,7 +45,7 @@ class Worker { * @param runtime, supplies the runtime for the server * @param random, supplies a random number generator */ - virtual void addListener(absl::optional overridden_listener, + virtual void addListener(std::optional overridden_listener, Network::ListenerConfig& listener, AddListenerCompletion completion, Runtime::Loader& runtime, Random::RandomGenerator& random) PURE; @@ -47,8 +58,10 @@ class Worker { * Start the worker thread. * @param guard_dog supplies the optional guard dog to use for thread watching. * @param cb a callback to run when the worker thread starts running. + * @param cpu_id an optional CPU to pin the worker thread to for CPU locality. */ - virtual void start(OptRef guard_dog, const std::function& cb) PURE; + virtual void start(OptRef guard_dog, const std::function& cb, + std::optional cpu_id) PURE; /** * Initialize stats for this worker's dispatcher, if available. The worker will output @@ -93,6 +106,25 @@ class Worker { virtual void stopListener(Network::ListenerConfig& listener, const Network::ExtraShutdownListenerOptions& options, std::function completion) PURE; + + /** + * Notify all connections in the given filter chains of the listener that they are being + * drained. This is intended to be invoked at the start of a drain sequence (before the + * drain timer expires). Connections are not closed. This is a fire-and-forget operation + * that is posted to the worker's dispatcher. + * @param listener_tag supplies the tag passed to addListener(). + * @param filter_chains supplies the filter chains whose connections should be notified. + */ + virtual void onFilterChainDrain(uint64_t listener_tag, + const std::list& filter_chains) PURE; + + /** + * Notify all connections of the given listener that they are being drained. Connections + * are not closed. This is a fire-and-forget operation that is posted to the worker's + * dispatcher. + * @param listener supplies the listener whose connections should be notified. + */ + virtual void onListenerDrain(Network::ListenerConfig& listener) PURE; }; using WorkerPtr = std::unique_ptr; diff --git a/envoy/ssl/BUILD b/envoy/ssl/BUILD index 321f796d794b4..60f0e214b74f8 100644 --- a/envoy/ssl/BUILD +++ b/envoy/ssl/BUILD @@ -18,7 +18,6 @@ envoy_cc_library( ":ssl_socket_state", "//envoy/common:optref_lib", "//envoy/common:time_interface", - "@abseil-cpp//absl/types:optional", ], ) @@ -67,7 +66,6 @@ envoy_cc_library( name = "certificate_validation_context_config_interface", hdrs = ["certificate_validation_context_config.h"], deps = [ - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", ], diff --git a/envoy/ssl/certificate_validation_context_config.h b/envoy/ssl/certificate_validation_context_config.h index eed50ab94b4b0..f336641caa5b4 100644 --- a/envoy/ssl/certificate_validation_context_config.h +++ b/envoy/ssl/certificate_validation_context_config.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -10,8 +11,6 @@ #include "envoy/extensions/transport_sockets/tls/v3/common.pb.h" #include "envoy/type/matcher/v3/string.pb.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Ssl { @@ -82,7 +81,7 @@ class CertificateValidationContextConfig { /** * @return the configuration for the custom certificate validator if configured. */ - virtual const absl::optional& + virtual const std::optional& customValidatorConfig() const PURE; /** @@ -98,7 +97,7 @@ class CertificateValidationContextConfig { /** * @return the max depth used when verifying the certificate-chain */ - virtual absl::optional maxVerifyDepth() const PURE; + virtual std::optional maxVerifyDepth() const PURE; /** * @return true if the SAN validation rules should be replaced with a rule to validate that the @@ -106,6 +105,11 @@ class CertificateValidationContextConfig { */ virtual bool autoSniSanMatch() const PURE; + /** + * @return whether to suppress sending CA certificate names to clients during handshake. + */ + virtual bool suppressClientCaList() const PURE; + // SECURITY NOTE // // When adding or changing this interface, it is likely that a change is needed to diff --git a/envoy/ssl/connection.h b/envoy/ssl/connection.h index 3d364b1fd5d3b..ae319f0f27e17 100644 --- a/envoy/ssl/connection.h +++ b/envoy/ssl/connection.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/pure.h" @@ -8,7 +9,6 @@ #include "envoy/ssl/parsed_x509_name.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" #include "absl/types/span.h" namespace Envoy { @@ -113,7 +113,7 @@ class ConnectionInfo { /** * @return the well-known attribute values parsed from subject field of the peer certificate. - * Returns absl::nullopt if there is no peer certificate. + * Returns std::nullopt if there is no peer certificate. **/ virtual ParsedX509NameOptConstRef parsedSubjectPeerCertificate() const PURE; @@ -223,16 +223,16 @@ class ConnectionInfo { virtual absl::Span oidsLocalCertificate() const PURE; /** - * @return absl::optional the time that the peer certificate was issued and should be - * considered valid from. Returns empty absl::optional if there is no peer certificate. + * @return std::optional the time that the peer certificate was issued and should be + * considered valid from. Returns empty std::optional if there is no peer certificate. **/ - virtual absl::optional validFromPeerCertificate() const PURE; + virtual std::optional validFromPeerCertificate() const PURE; /** - * @return absl::optional the time that the peer certificate expires and should not be - * considered valid after. Returns empty absl::optional if there is no peer certificate. + * @return std::optional the time that the peer certificate expires and should not be + * considered valid after. Returns empty std::optional if there is no peer certificate. **/ - virtual absl::optional expirationPeerCertificate() const PURE; + virtual std::optional expirationPeerCertificate() const PURE; /** * @return std::string the hex-encoded TLS session ID as defined in rfc5246. @@ -251,6 +251,18 @@ class ConnectionInfo { **/ virtual std::string ciphersuiteString() const PURE; + /** + * @return uint16_t the OpenSSL id of the group that was used for the key agreement of the + * established TLS connection. Returns 0 if there is no group. + **/ + virtual uint16_t tlsGroupId() const PURE; + + /** + * @return absl::string_view the OpenSSL name of the group that was used for the key agreement of + * the established TLS connection. Returns "" if there is no group. + **/ + virtual absl::string_view tlsGroupString() const PURE; + /** * @return std::string the TLS version (e.g., TLSv1.2, TLSv1.3) used in the established TLS * connection. diff --git a/envoy/ssl/context.h b/envoy/ssl/context.h index 7cb4e36f175f0..9429d6d496ba2 100644 --- a/envoy/ssl/context.h +++ b/envoy/ssl/context.h @@ -1,14 +1,13 @@ #pragma once #include +#include #include #include "envoy/admin/v3/certs.pb.h" #include "envoy/common/pure.h" #include "envoy/common/time.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Ssl { @@ -25,7 +24,7 @@ class Context { * @return the number of days in this context until the next certificate will expire, the value is * set when not expired. */ - virtual absl::optional daysUntilFirstCertExpires() const PURE; + virtual std::optional daysUntilFirstCertExpires() const PURE; /** * @return certificate details conforming to proto admin.v2alpha.certs. @@ -39,9 +38,9 @@ class Context { /** * @return the number of seconds in this context until the next OCSP response will - * expire, or `absl::nullopt` if no OCSP responses exist. + * expire, or `std::nullopt` if no OCSP responses exist. */ - virtual absl::optional secondsUntilFirstOcspResponseExpires() const PURE; + virtual std::optional secondsUntilFirstOcspResponseExpires() const PURE; }; using ContextSharedPtr = std::shared_ptr; diff --git a/envoy/ssl/context_config.h b/envoy/ssl/context_config.h index dba2861df080e..7cc6dc90a9a89 100644 --- a/envoy/ssl/context_config.h +++ b/envoy/ssl/context_config.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -14,8 +15,6 @@ #include "source/common/network/cidr_range.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Ssl { @@ -122,7 +121,7 @@ class ContextConfig { /** * @return the compiance policy for the TLS context. */ - virtual absl::optional< + virtual std::optional< envoy::extensions::transport_sockets::tls::v3::TlsParameters::CompliancePolicy> compliancePolicy() const PURE; }; @@ -158,12 +157,6 @@ class ClientContextConfig : public virtual ContextConfig { */ virtual size_t maxSessionKeys() const PURE; - /** - * @return true if the enforcement that handshake will fail if the keyUsage extension is present - * and incompatible with the TLS usage is enabled. - */ - virtual bool enforceRsaKeyUsage() const PURE; - /** * @return an optional factory which can be used to create TLS context provider instances. */ @@ -208,7 +201,7 @@ class ServerContextConfig : public virtual ContextConfig { * @return timeout in seconds for the session. * Session timeout is used to specify lifetime hint of tls tickets. */ - virtual absl::optional sessionTimeout() const PURE; + virtual std::optional sessionTimeout() const PURE; /** * @return True if stateless TLS session resumption is disabled, false otherwise. diff --git a/envoy/ssl/context_manager.h b/envoy/ssl/context_manager.h index d4fdd5dc87cd6..d05b14a3f826b 100644 --- a/envoy/ssl/context_manager.h +++ b/envoy/ssl/context_manager.h @@ -46,7 +46,7 @@ class ContextManager { * @return the number of days until the next certificate being managed will expire, the value is * set when not expired. */ - virtual absl::optional daysUntilFirstCertExpires() const PURE; + virtual std::optional daysUntilFirstCertExpires() const PURE; /** * Iterates through the contexts currently attached to a listener. @@ -61,9 +61,9 @@ class ContextManager { /** * @return the number of seconds until the next OCSP response being managed will - * expire, or `absl::nullopt` if no OCSP responses exist. + * expire, or `std::nullopt` if no OCSP responses exist. */ - virtual absl::optional secondsUntilFirstOcspResponseExpires() const PURE; + virtual std::optional secondsUntilFirstOcspResponseExpires() const PURE; /** * Remove an existing ssl context. diff --git a/envoy/ssl/ssl_socket_state.h b/envoy/ssl/ssl_socket_state.h index aa60fbc178ab9..e0ab9e8a02ddd 100644 --- a/envoy/ssl/ssl_socket_state.h +++ b/envoy/ssl/ssl_socket_state.h @@ -3,7 +3,21 @@ namespace Envoy { namespace Ssl { -enum class SocketState { PreHandshake, HandshakeInProgress, HandshakeComplete, ShutdownSent }; +enum class SocketState { + // The handshake is in progress and waiting on data on the connection, either to be sent or + // received. + HandshakeWaitingForConnectionData, + + // The handshake is in progress and waiting on Envoy to complete an operation before it can + // continue. + HandshakeBlockedOnAsyncOperation, + + // The handshake is complete. + HandshakeComplete, + + // A shutdown signal has been sent on the connection. + ShutdownSent +}; } // namespace Ssl } // namespace Envoy diff --git a/envoy/stats/BUILD b/envoy/stats/BUILD index 8550b7949f3ea..d5bcb08da9f19 100644 --- a/envoy/stats/BUILD +++ b/envoy/stats/BUILD @@ -17,28 +17,91 @@ envoy_cc_library( envoy_cc_library( name = "tag_interface", hdrs = ["tag.h"], + deps = [ + "@abseil-cpp//absl/container:inlined_vector", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/types:span", + ], +) + +envoy_cc_library( + name = "store_interface", + hdrs = ["store.h"], + deps = [ + ":scope_interface", + ":stats_interface", + ":stats_matcher_interface", + ":tag_producer_interface", + "//envoy/common:optref_lib", + "//envoy/common:pure_lib", + "//envoy/event:dispatcher_interface", + "//envoy/thread_local:thread_local_interface", + ], +) + +envoy_cc_library( + name = "scope_interface", + hdrs = ["scope.h"], + deps = [ + ":histogram_interface", + ":refcount_ptr_interface", + ":stats_matcher_interface", + ":tag_interface", + "//envoy/common:pure_lib", + "//source/common/stats:symbol_table_lib", + ], +) + +envoy_cc_library( + name = "stats_matcher_interface", + hdrs = ["stats_matcher.h"], + deps = [ + "//envoy/common:pure_lib", + ], +) + +envoy_cc_library( + name = "histogram_interface", + hdrs = ["histogram.h"], + deps = [ + ":refcount_ptr_interface", + ":stats_interface", + "//envoy/common:pure_lib", + "@abseil-cpp//absl/strings", + ], +) + +envoy_cc_library( + name = "tag_extractor_interface", + hdrs = ["tag_extractor.h"], + deps = [ + ":tag_interface", + "//envoy/common:interval_set_interface", + "//envoy/common:pure_lib", + "@abseil-cpp//absl/strings", + ], +) + +envoy_cc_library( + name = "tag_producer_interface", + hdrs = ["tag_producer.h"], + deps = [ + ":tag_interface", + "//envoy/common:pure_lib", + "@abseil-cpp//absl/strings", + ], ) -# TODO(jmarantz): atomize the build rules to match the include files. envoy_cc_library( name = "stats_interface", hdrs = [ - "allocator.h", - "histogram.h", - "scope.h", "stats.h", - "stats_matcher.h", - "store.h", - "tag_extractor.h", - "tag_producer.h", ], deps = [ ":refcount_ptr_interface", ":tag_interface", - "//envoy/common:interval_set_interface", - "//envoy/common:optref_lib", - "//envoy/common:time_interface", - "@abseil-cpp//absl/container:inlined_vector", + "//envoy/common:pure_lib", + "@abseil-cpp//absl/strings", ], ) diff --git a/envoy/stats/allocator.h b/envoy/stats/allocator.h deleted file mode 100644 index 3c22bbd028d66..0000000000000 --- a/envoy/stats/allocator.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -// This header is a placeholder which we will carry until June 1, 2026, -// as we have deprecated the pure interface and impl pattern. -// -// Please remove references to this file and instead include -// source/common/stats/allocator.h. - -namespace Envoy { -namespace Stats { - -class Allocator; - -} // namespace Stats -} // namespace Envoy diff --git a/envoy/stats/custom_stat_namespaces.h b/envoy/stats/custom_stat_namespaces.h index 175a80efe6b7a..8376b3db33585 100644 --- a/envoy/stats/custom_stat_namespaces.h +++ b/envoy/stats/custom_stat_namespaces.h @@ -1,9 +1,10 @@ #pragma once +#include + #include "envoy/common/pure.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Stats { @@ -44,7 +45,7 @@ class CustomStatNamespaces { * @return the stripped string if stat_name has a registered custom stat namespace. Otherwise, * return nullopt. */ - virtual absl::optional + virtual std::optional stripRegisteredPrefix(const absl::string_view stat_name) const PURE; }; diff --git a/envoy/stats/histogram.h b/envoy/stats/histogram.h index e052f74fe5ff4..be18a1fe7c8be 100644 --- a/envoy/stats/histogram.h +++ b/envoy/stats/histogram.h @@ -2,12 +2,16 @@ #include #include +#include +#include #include #include "envoy/common/pure.h" #include "envoy/stats/refcount_ptr.h" #include "envoy/stats/stats.h" +#include "absl/strings/string_view.h" + namespace Envoy { namespace Stats { @@ -30,7 +34,7 @@ class HistogramSettings { * version of the histogram). * @return An optional override for the number of bins. */ - virtual absl::optional bins(absl::string_view stat_name) const PURE; + virtual std::optional bins(absl::string_view stat_name) const PURE; }; using HistogramSettingsConstPtr = std::unique_ptr; diff --git a/envoy/stats/primitive_stats.h b/envoy/stats/primitive_stats.h index be210b581b998..659e09672e5dc 100644 --- a/envoy/stats/primitive_stats.h +++ b/envoy/stats/primitive_stats.h @@ -69,6 +69,9 @@ class PrimitiveMetricMetadata { const std::string& tagExtractedName() const { return tag_extracted_name_; } const std::string& name() const { return name_; } const Stats::TagVector& tags() const { return tags_; } + + // Returns the current tags, leaves tags_ in an unknown state. + Stats::TagVector getAndClearTags() { return std::move(tags_); } bool used() const { return true; } bool hidden() const { return false; } diff --git a/envoy/stats/scope.h b/envoy/stats/scope.h index 895f22e6965e1..75c23a0e76a5e 100644 --- a/envoy/stats/scope.h +++ b/envoy/stats/scope.h @@ -3,13 +3,16 @@ #include #include #include +#include +#include #include "envoy/common/pure.h" #include "envoy/stats/histogram.h" +#include "envoy/stats/refcount_ptr.h" #include "envoy/stats/stats_matcher.h" #include "envoy/stats/tag.h" -#include "absl/types/optional.h" +#include "source/common/stats/symbol_table.h" namespace Envoy { namespace Stats { @@ -22,22 +25,22 @@ class Scope; class Store; class TextReadout; -using CounterOptConstRef = absl::optional>; -using GaugeOptConstRef = absl::optional>; -using HistogramOptConstRef = absl::optional>; -using TextReadoutOptConstRef = absl::optional>; +using CounterOptConstRef = std::optional>; +using GaugeOptConstRef = std::optional>; +using HistogramOptConstRef = std::optional>; +using TextReadoutOptConstRef = std::optional>; using ConstScopeSharedPtr = std::shared_ptr; using ScopeSharedPtr = std::shared_ptr; // Settings for limiting the number of counters, gauges and histograms allowed // in a scope. This currently only supports thread local stats. struct ScopeStatsLimitSettings { - // Max number of counters allowed in this scope. 0 means no limit. - uint32_t max_counters = 0; - // Max number of gauges allowed in this scope. 0 means no limit. - uint32_t max_gauges = 0; - // Max number of histograms allowed in this scope. 0 means no limit. - uint32_t max_histograms = 0; + // Max number of counters allowed in this scope. std::nullopt means no limit. + std::optional max_counters = std::nullopt; + // Max number of gauges allowed in this scope. std::nullopt means no limit. + std::optional max_gauges = std::nullopt; + // Max number of histograms allowed in this scope. std::nullopt means no limit. + std::optional max_histograms = std::nullopt; }; template using IterateFn = std::function&)>; @@ -75,6 +78,12 @@ class Scope : public std::enable_shared_from_this { /** @return a const shared_ptr for this */ ConstScopeSharedPtr getConstShared() const { return shared_from_this(); } + /** + * Set a callback to be run when the scope is destroyed. + * @param callback the callback to run. + */ + virtual void setCleanupCallback(std::function callback) = 0; + /** * Allocate a new scope. NOTE: The implementation should correctly handle overlapping scopes * that point to the same reference counted backing stats. This allows a new scope to be @@ -90,9 +99,33 @@ class Scope : public std::enable_shared_from_this { * NOTE: If the scope specific matcher is set, then the sub scope will inherit the same matcher * unless another matcher is explicitly set. */ - virtual ScopeSharedPtr createScope(const std::string& name, bool evictable = false, - const ScopeStatsLimitSettings& limits = {}, - StatsMatcherSharedPtr matcher = nullptr) PURE; + ScopeSharedPtr createScope(absl::string_view name, bool evictable = false, + const ScopeStatsLimitSettings& limits = {}, + StatsMatcherSharedPtr matcher = nullptr) { + return createScopeWithTaggedName(name, {}, absl::string_view{}, evictable, limits, + std::move(matcher)); + } + + /** + * Allocate a new scope, optionally supplying tags and a pre-built tagged name (with tag values + * interleaved). NOTE: The behavior is implementation-defined for now because both legacy and + * tag-aware scopes are still supported. + * + * @param base_name supplies the scope's tag-extracted name. + * @param name_tags tags to associate with (and propagate from) the scope, as string_view pairs. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * @param evictable whether unused metrics can be deleted from the scope caches. + * @param limits metric limits for counters, gauges and histograms allowed in this scope. + * @param matcher optional per-scope stats matcher; replaces the store-level matcher when set. + */ + virtual ScopeSharedPtr createScopeWithTaggedName(absl::string_view base_name, + TagStringViewSpan name_tags, + absl::string_view tagged_name, + bool evictable = false, + const ScopeStatsLimitSettings& limits = {}, + StatsMatcherSharedPtr matcher = nullptr) PURE; /** * Allocate a new scope. NOTE: The implementation should correctly handle overlapping scopes @@ -107,9 +140,93 @@ class Scope : public std::enable_shared_from_this { * NOTE: If the scope specific matcher is set, then the sub scope will inherit the same matcher * unless another matcher is explicitly set. */ - virtual ScopeSharedPtr scopeFromStatName(StatName name, bool evictable = false, - const ScopeStatsLimitSettings& limits = {}, - StatsMatcherSharedPtr matcher = nullptr) PURE; + ScopeSharedPtr scopeFromStatName(StatName name, bool evictable = false, + const ScopeStatsLimitSettings& limits = {}, + StatsMatcherSharedPtr matcher = nullptr) { + return scopeFromTaggedName(name, {}, StatName(), evictable, limits, std::move(matcher)); + } + + /** + * Allocate a new scope from a StatName, optionally supplying tags and a pre-built tagged name + * (with tag values interleaved). See the `createScopeWithTaggedName` variant for details and + * notes. + * + * @param base_name supplies the scope's tag-extracted name. + * @param name_tags tags to associate with (and propagate from) the scope. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * @param evictable whether unused metrics can be deleted from the scope caches. + * @param limits metric limits for counters, gauges and histograms allowed in this scope. + * @param matcher optional per-scope stats matcher; replaces the store-level matcher when set. + */ + virtual ScopeSharedPtr scopeFromTaggedName(StatName base_name, StatNameTagSpan name_tags, + StatName tagged_name, bool evictable = false, + const ScopeStatsLimitSettings& limits = {}, + StatsMatcherSharedPtr matcher = nullptr) PURE; + + /** + * Creates a Counter from the tag-extracted name, tags and an optional pre-built tagged name. + * @param base_name The tag-extracted name of the stat, obtained from the SymbolTable. + * @param name_tags optionally specified tags. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * + * @return a counter within the scope's namespace. + */ + virtual Counter& counterFromTaggedName(StatName base_name, + std::optional name_tags, + StatName tagged_name) PURE; + + /** + * Creates a Gauge from the tag-extracted name, tags and an optional pre-built tagged name. + * See the `counterFromTaggedName` variant for details and notes on name_tags and tagged_name. + * + * @param base_name The tag-extracted name of the stat (no tag values), obtained from the + * SymbolTable. + * @param name_tags optionally specified tags. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * @param import_mode Whether hot-restart should accumulate this value. + * @return a gauge within the scope's namespace. + */ + virtual Gauge& gaugeFromTaggedName(StatName base_name, std::optional name_tags, + StatName tagged_name, Gauge::ImportMode import_mode) PURE; + + /** + * Creates a Histogram from the tag-extracted name, tags and an optional pre-built tagged name. + * See the `counterFromTaggedName` variant for details and notes on name_tags and tagged_name. + * + * @param base_name The tag-extracted name of the stat (no tag values), obtained from the + * SymbolTable. + * @param name_tags optionally specified tags. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * @param unit The unit of measurement. + * @return a histogram within the scope's namespace with a particular value type. + */ + virtual Histogram& histogramFromTaggedName(StatName base_name, + std::optional name_tags, + StatName tagged_name, Histogram::Unit unit) PURE; + + /** + * Creates a TextReadout from the tag-extracted name, tags and an optional pre-built tagged name. + * See the `counterFromTaggedName` variant for details and notes on name_tags and tagged_name. + * + * @param base_name The tag-extracted name of the stat (no tag values), obtained from the + * SymbolTable. + * @param name_tags optionally specified tags. + * @param tagged_name tagged_name optional explicit flat name with tag values included in the + * specified order. If tagged_name is empty, the scope implementation may derive the tagged name + * by joining the base_name and name_tags. + * @return a text readout within the scope's namespace. + */ + virtual TextReadout& textReadoutFromTaggedName(StatName base_name, + std::optional name_tags, + StatName tagged_name) PURE; /** * Creates a Counter from the stat name. Tag extraction will be performed on the name. @@ -117,7 +234,7 @@ class Scope : public std::enable_shared_from_this { * @return a counter within the scope's namespace. */ Counter& counterFromStatName(const StatName& name) { - return counterFromStatNameWithTags(name, absl::nullopt); + return counterFromStatNameWithTags(name, std::nullopt); } /** * Creates a Counter from the stat name and tags. If tags are not provided, tag extraction @@ -126,8 +243,9 @@ class Scope : public std::enable_shared_from_this { * @param tags optionally specified tags. * @return a counter within the scope's namespace. */ - virtual Counter& counterFromStatNameWithTags(const StatName& name, - StatNameTagVectorOptConstRef tags) PURE; + Counter& counterFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags) { + return counterFromTaggedName(name, toTagSpan(tags), StatName()); + } /** * TODO(#6667): this variant is deprecated: use counterFromStatName. @@ -143,7 +261,7 @@ class Scope : public std::enable_shared_from_this { * @return a gauge within the scope's namespace. */ Gauge& gaugeFromStatName(const StatName& name, Gauge::ImportMode import_mode) { - return gaugeFromStatNameWithTags(name, absl::nullopt, import_mode); + return gaugeFromStatNameWithTags(name, std::nullopt, import_mode); } /** @@ -154,8 +272,10 @@ class Scope : public std::enable_shared_from_this { * @param import_mode Whether hot-restart should accumulate this value. * @return a gauge within the scope's namespace. */ - virtual Gauge& gaugeFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags, - Gauge::ImportMode import_mode) PURE; + Gauge& gaugeFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags, + Gauge::ImportMode import_mode) { + return gaugeFromTaggedName(name, toTagSpan(tags), StatName(), import_mode); + } /** * TODO(#6667): this variant is deprecated: use gaugeFromStatName. @@ -172,7 +292,7 @@ class Scope : public std::enable_shared_from_this { * @return a histogram within the scope's namespace with a particular value type. */ Histogram& histogramFromStatName(const StatName& name, Histogram::Unit unit) { - return histogramFromStatNameWithTags(name, absl::nullopt, unit); + return histogramFromStatNameWithTags(name, std::nullopt, unit); } /** @@ -183,9 +303,10 @@ class Scope : public std::enable_shared_from_this { * @param unit The unit of measurement. * @return a histogram within the scope's namespace with a particular value type. */ - virtual Histogram& histogramFromStatNameWithTags(const StatName& name, - StatNameTagVectorOptConstRef tags, - Histogram::Unit unit) PURE; + Histogram& histogramFromStatNameWithTags(const StatName& name, StatNameTagVectorOptConstRef tags, + Histogram::Unit unit) { + return histogramFromTaggedName(name, toTagSpan(tags), StatName(), unit); + } /** * TODO(#6667): this variant is deprecated: use histogramFromStatName. @@ -201,7 +322,7 @@ class Scope : public std::enable_shared_from_this { * @return a text readout within the scope's namespace. */ TextReadout& textReadoutFromStatName(const StatName& name) { - return textReadoutFromStatNameWithTags(name, absl::nullopt); + return textReadoutFromStatNameWithTags(name, std::nullopt); } /** @@ -211,8 +332,10 @@ class Scope : public std::enable_shared_from_this { * @param tags optionally specified tags. * @return a text readout within the scope's namespace. */ - virtual TextReadout& textReadoutFromStatNameWithTags(const StatName& name, - StatNameTagVectorOptConstRef tags) PURE; + TextReadout& textReadoutFromStatNameWithTags(const StatName& name, + StatNameTagVectorOptConstRef tags) { + return textReadoutFromTaggedName(name, toTagSpan(tags), StatName()); + } /** * TODO(#6667): this variant is deprecated: use textReadoutFromStatName. @@ -300,6 +423,17 @@ class Scope : public std::enable_shared_from_this { */ virtual Store& store() PURE; virtual const Store& constStore() const PURE; + + // Converts the legacy optional-vector-reference tag representation accepted by the deprecated + // *FromStatNameWithTags convenience methods into the span representation taken by the new + // *FromStatName virtual methods. The returned span aliases the caller-owned vector and must only + // be used for the duration of the (synchronous) call. + static std::optional toTagSpan(StatNameTagVectorOptConstRef tags) { + if (tags.has_value()) { + return StatNameTagSpan(tags->get()); + } + return std::nullopt; + } }; } // namespace Stats diff --git a/envoy/stats/stats_matcher.h b/envoy/stats/stats_matcher.h index 9b30df2070d87..0826c834c3dda 100644 --- a/envoy/stats/stats_matcher.h +++ b/envoy/stats/stats_matcher.h @@ -1,8 +1,6 @@ #pragma once #include -#include -#include #include "envoy/common/pure.h" diff --git a/envoy/stats/store.h b/envoy/stats/store.h index 86f9a5ad0efe4..d2f4ceedc6fcb 100644 --- a/envoy/stats/store.h +++ b/envoy/stats/store.h @@ -6,22 +6,15 @@ #include "envoy/common/optref.h" #include "envoy/common/pure.h" +#include "envoy/event/dispatcher.h" #include "envoy/stats/histogram.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats.h" #include "envoy/stats/stats_matcher.h" #include "envoy/stats/tag_producer.h" +#include "envoy/thread_local/thread_local.h" namespace Envoy { -namespace Event { - -class Dispatcher; -} - -namespace ThreadLocal { -class Instance; -} - namespace Stats { class Sink; @@ -177,11 +170,18 @@ class Store { /** * @return a scope of the given name. */ - ScopeSharedPtr createScope(const std::string& name, bool evictable = false, + ScopeSharedPtr createScope(absl::string_view name, bool evictable = false, const ScopeStatsLimitSettings& limits = {}, StatsMatcherSharedPtr matcher = nullptr) { return rootScope()->createScope(name, evictable, limits, std::move(matcher)); } + ScopeSharedPtr createScopeWithTaggedName(absl::string_view base_name, TagStringViewSpan name_tags, + absl::string_view tagged_name, bool evictable = false, + const ScopeStatsLimitSettings& limits = {}, + StatsMatcherSharedPtr matcher = nullptr) { + return rootScope()->createScopeWithTaggedName(base_name, name_tags, tagged_name, evictable, + limits, std::move(matcher)); + } /** * Extracts tags from the name and appends them to the provided StatNameTagVector. diff --git a/envoy/stats/tag.h b/envoy/stats/tag.h index 9916b71f46e03..90808e59595ed 100644 --- a/envoy/stats/tag.h +++ b/envoy/stats/tag.h @@ -1,8 +1,14 @@ #pragma once +#include +#include #include +#include +#include -#include "absl/types/optional.h" +#include "absl/container/inlined_vector.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" namespace Envoy { namespace Stats { @@ -25,8 +31,15 @@ using TagVector = std::vector; using StatNameTag = std::pair; using StatNameTagVector = std::vector; -using StatNameTagVectorOptConstRef = - absl::optional>; +using StatNameTagVectorOptConstRef = std::optional>; + +using StatNameTagSpan = absl::Span; +using StatNameTagVec = absl::InlinedVector; + +// String-view representation of a tag (name, value), used by the string-based scope APIs so +// callers can supply tags without first interning them in the symbol table. +using TagStringView = std::pair; +using TagStringViewSpan = absl::Span; } // namespace Stats } // namespace Envoy diff --git a/envoy/stats/tag_extractor.h b/envoy/stats/tag_extractor.h index f68deefc60ce6..01dd8ff3dcb14 100644 --- a/envoy/stats/tag_extractor.h +++ b/envoy/stats/tag_extractor.h @@ -1,8 +1,7 @@ #pragma once +#include #include -#include -#include #include "envoy/common/interval_set.h" #include "envoy/common/pure.h" diff --git a/envoy/stats/tag_producer.h b/envoy/stats/tag_producer.h index 02fe955015542..b8c33a88951c5 100644 --- a/envoy/stats/tag_producer.h +++ b/envoy/stats/tag_producer.h @@ -2,7 +2,6 @@ #include #include -#include #include "envoy/common/pure.h" #include "envoy/stats/tag.h" diff --git a/envoy/stream_info/BUILD b/envoy/stream_info/BUILD index c538b70a946cf..7ada0e5128d93 100644 --- a/envoy/stream_info/BUILD +++ b/envoy/stream_info/BUILD @@ -24,7 +24,6 @@ envoy_cc_library( "//source/common/common:assert_lib", "//source/common/protobuf", "//source/common/singleton:const_singleton", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -37,7 +36,6 @@ envoy_cc_library( "//source/common/common:fmt_lib", "//source/common/common:utility_lib", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/stream_info/filter_state.h b/envoy/stream_info/filter_state.h index 55bc1f21e945f..8b99c89825a75 100644 --- a/envoy/stream_info/filter_state.h +++ b/envoy/stream_info/filter_state.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/pure.h" @@ -11,7 +12,6 @@ #include "source/common/protobuf/protobuf.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace StreamInfo { @@ -52,8 +52,6 @@ enum class StreamSharingMayImpactPooling { */ class FilterState { public: - enum class StateType { ReadOnly, Mutable }; - // Objects stored in the FilterState may have different life span. Life span is what controls // how long an object stored in FilterState lives. Implementation of this interface actually // stores objects in a (reverse) tree manner - multiple FilterStateImpl with shorter life span may @@ -99,11 +97,11 @@ class FilterState { virtual ProtobufTypes::MessagePtr serializeAsProto() const { return nullptr; } /** - * @return absl::optional a optional string to the serialization of the filter + * @return std::optional a optional string to the serialization of the filter * state. No value if the filter state cannot be serialized or serialization is not supported. * This method can be used to get an unstructured serialization result. */ - virtual absl::optional serializeAsString() const { return absl::nullopt; } + virtual std::optional serializeAsString() const { return std::nullopt; } /** * @return bool true if the object supports field access. False if the object does not support @@ -136,7 +134,6 @@ class FilterState { struct FilterObject { std::shared_ptr data_; - StateType state_type_{StateType::ReadOnly}; StreamSharingMayImpactPooling stream_sharing_{StreamSharingMayImpactPooling::None}; std::string name_; }; @@ -149,24 +146,19 @@ class FilterState { /** * @param data_name the name of the data being set. * @param data an owning pointer to the data to be stored. - * @param state_type indicates whether the object is mutable or not. * @param life_span indicates the life span of the object: bound to the filter chain, a * request, or a connection. * - * Note that it is an error to call setData() twice with the same - * data_name, if the existing object is immutable. Similarly, it is an - * error to call setData() with same data_name but different state_types - * (mutable and readOnly, or readOnly and mutable) or different life_span. - * This is to enforce a single authoritative source for each piece of - * data stored in FilterState. + * Note that it is an error to call setData() twice with the same data_name, but different + * life_span. */ virtual void - setData(absl::string_view data_name, std::shared_ptr data, StateType state_type, + setData(absl::string_view data_name, std::shared_ptr data, LifeSpan life_span = LifeSpan::FilterChain, StreamSharingMayImpactPooling stream_sharing = StreamSharingMayImpactPooling::None) PURE; /** - * @param data_name the name of the data being looked up (mutable/readonly). + * @param data_name the name of the data being looked up. * @return a typed pointer to the stored data or nullptr if the data does not exist or the data * type does not match the expected type. */ @@ -175,13 +167,13 @@ class FilterState { } /** - * @param data_name the name of the data being looked up (mutable/readonly). + * @param data_name the name of the data being looked up. * @return a const pointer to the stored data or nullptr if the data does not exist. */ virtual const Object* getDataReadOnlyGeneric(absl::string_view data_name) const PURE; /** - * @param data_name the name of the data being looked up (mutable/readonly). + * @param data_name the name of the data being looked up. * @return a typed pointer to the stored data or nullptr if the data does not exist or the data * type does not match the expected type. */ @@ -190,13 +182,13 @@ class FilterState { } /** - * @param data_name the name of the data being looked up (mutable/readonly). + * @param data_name the name of the data being looked up. * @return a pointer to the stored data or nullptr if the data does not exist. */ virtual Object* getDataMutableGeneric(absl::string_view data_name) PURE; /** - * @param data_name the name of the data being looked up (mutable/readonly). + * @param data_name the name of the data being looked up. * @return a shared pointer to the stored data or nullptr if the data does not exist. */ virtual std::shared_ptr getDataSharedMutableGeneric(absl::string_view data_name) PURE; diff --git a/envoy/stream_info/stream_id_provider.h b/envoy/stream_info/stream_id_provider.h index 445fe3a59885e..9bd0a98e32056 100644 --- a/envoy/stream_info/stream_id_provider.h +++ b/envoy/stream_info/stream_id_provider.h @@ -1,11 +1,11 @@ #pragma once #include +#include #include "envoy/common/pure.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace StreamInfo { @@ -23,12 +23,12 @@ class StreamIdProvider { /** * @return the optional string view of the stream id. */ - virtual absl::optional toStringView() const PURE; + virtual std::optional toStringView() const PURE; /** * @return the optional integer view of the stream id. */ - virtual absl::optional toInteger() const PURE; + virtual std::optional toInteger() const PURE; }; using StreamIdProviderSharedPtr = std::shared_ptr; diff --git a/envoy/stream_info/stream_info.h b/envoy/stream_info/stream_info.h index 5675997e9def8..7303b6cec7082 100644 --- a/envoy/stream_info/stream_info.h +++ b/envoy/stream_info/stream_info.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/common/pure.h" @@ -20,8 +21,6 @@ #include "source/common/protobuf/protobuf.h" #include "source/common/singleton/const_singleton.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Router { @@ -283,6 +282,7 @@ struct LocalCloseReasonValues { const std::string TransportSocketTimeout = "transport_socket_timeout"; const std::string TriggeredDelayedCloseTimeout = "triggered_delayed_close_timeout"; const std::string TcpProxyInitializationFailure = "tcp_initializion_failure:"; + const std::string TcpProxyDrainClose = "tcp_proxy_drain_close"; const std::string TcpSessionIdleTimeout = "tcp_session_idle_timeout"; const std::string MaxConnectionDurationReached = "max_connection_duration_reached"; const std::string ClosingUpstreamTcpDueToDownstreamRemoteClose = @@ -362,30 +362,30 @@ struct UpstreamTiming { upstream_handshake_complete_ = time_source.monotonicTime(); } - absl::optional upstreamHandshakeComplete() const { + std::optional upstreamHandshakeComplete() const { return upstream_handshake_complete_; } - absl::optional connectionPoolCallbackLatency() const { + std::optional connectionPoolCallbackLatency() const { return connection_pool_callback_latency_; } - absl::optional connection_pool_callback_latency_; - absl::optional first_upstream_tx_byte_sent_; - absl::optional last_upstream_tx_byte_sent_; - absl::optional first_upstream_rx_byte_received_; - absl::optional last_upstream_rx_byte_received_; - absl::optional first_upstream_rx_body_byte_received_; + std::optional connection_pool_callback_latency_; + std::optional first_upstream_tx_byte_sent_; + std::optional last_upstream_tx_byte_sent_; + std::optional first_upstream_rx_byte_received_; + std::optional last_upstream_rx_byte_received_; + std::optional first_upstream_rx_body_byte_received_; - absl::optional upstream_connect_start_; - absl::optional upstream_connect_complete_; - absl::optional upstream_handshake_complete_; + std::optional upstream_connect_start_; + std::optional upstream_connect_complete_; + std::optional upstream_handshake_complete_; }; struct DownstreamTiming { void setValue(absl::string_view key, MonotonicTime value) { timings_[key] = value; } - absl::optional getValue(absl::string_view value) const { + std::optional getValue(absl::string_view value) const { auto ret = timings_.find(value); if (ret == timings_.end()) { return {}; @@ -393,24 +393,30 @@ struct DownstreamTiming { return ret->second; } - absl::optional lastDownstreamRxByteReceived() const { + std::optional lastDownstreamRxByteReceived() const { return last_downstream_rx_byte_received_; } - absl::optional firstDownstreamTxByteSent() const { + std::optional firstDownstreamTxByteSent() const { return first_downstream_tx_byte_sent_; } - absl::optional lastDownstreamTxByteSent() const { + std::optional lastDownstreamTxByteSent() const { return last_downstream_tx_byte_sent_; } - absl::optional downstreamHandshakeComplete() const { + std::optional downstreamHandshakeComplete() const { return downstream_handshake_complete_; } - absl::optional lastDownstreamAckReceived() const { + std::optional lastDownstreamAckReceived() const { return last_downstream_ack_received_; } - absl::optional lastDownstreamHeaderRxByteReceived() const { + std::optional lastDownstreamHeaderRxByteReceived() const { return last_downstream_header_rx_byte_received_; } + std::optional downstreamConnectionBegin() const { + return downstream_connection_begin_; + } + std::optional downstreamConnectionEnd() const { + return downstream_connection_end_; + } void onLastDownstreamRxByteReceived(TimeSource& time_source) { ASSERT(!last_downstream_rx_byte_received_); @@ -436,20 +442,34 @@ struct DownstreamTiming { ASSERT(!last_downstream_header_rx_byte_received_); last_downstream_header_rx_byte_received_ = time_source.monotonicTime(); } + void setDownstreamConnectionBegin(MonotonicTime time) { + ASSERT(!downstream_connection_begin_); + downstream_connection_begin_ = time; + } + void onDownstreamConnectionEnd(TimeSource& time_source) { + // Record only the first close so the connection duration is not inflated. + if (!downstream_connection_end_) { + downstream_connection_end_ = time_source.monotonicTime(); + } + } absl::flat_hash_map timings_; // The time when the last byte of the request was received. - absl::optional last_downstream_rx_byte_received_; + std::optional last_downstream_rx_byte_received_; // The time when the first byte of the response was sent downstream. - absl::optional first_downstream_tx_byte_sent_; + std::optional first_downstream_tx_byte_sent_; // The time when the last byte of the response was sent downstream. - absl::optional last_downstream_tx_byte_sent_; + std::optional last_downstream_tx_byte_sent_; // The time the TLS handshake completed. Set at connection level. - absl::optional downstream_handshake_complete_; + std::optional downstream_handshake_complete_; // The time the final ack was received from the client. - absl::optional last_downstream_ack_received_; + std::optional last_downstream_ack_received_; // The time when the last header byte was received. - absl::optional last_downstream_header_rx_byte_received_; + std::optional last_downstream_header_rx_byte_received_; + // The time the downstream connection was established. + std::optional downstream_connection_begin_; + // The time the downstream connection was closed. + std::optional downstream_connection_end_; }; // Measure the number of bytes sent and received for a stream. @@ -571,9 +591,9 @@ class UpstreamInfo { virtual void setUpstreamConnectionId(uint64_t id) PURE; /** - * @return the ID of the upstream connection, or absl::nullopt if not available. + * @return the ID of the upstream connection, or std::nullopt if not available. */ - virtual absl::optional upstreamConnectionId() const PURE; + virtual std::optional upstreamConnectionId() const PURE; /** * @param interface name of the upstream connection's local socket. @@ -581,10 +601,10 @@ class UpstreamInfo { virtual void setUpstreamInterfaceName(absl::string_view interface_name) PURE; /** - * @return interface name of the upstream connection's local socket, or absl::nullopt if not + * @return interface name of the upstream connection's local socket, or std::nullopt if not * available. */ - virtual absl::optional upstreamInterfaceName() const PURE; + virtual std::optional upstreamInterfaceName() const PURE; /** * @param connection_info sets the upstream ssl connection. @@ -687,7 +707,7 @@ class UpstreamInfo { virtual uint64_t upstreamNumStreams() const PURE; virtual void setUpstreamProtocol(Http::Protocol protocol) PURE; - virtual absl::optional upstreamProtocol() const PURE; + virtual std::optional upstreamProtocol() const PURE; /** * Add a host to the list of upstream hosts that were attempted for this request. @@ -760,12 +780,12 @@ class StreamInfo { /** * @param std::string name denotes the name of the virtual cluster. */ - virtual void setVirtualClusterName(const absl::optional& name) PURE; + virtual void setVirtualClusterName(const std::optional& name) PURE; /** * @return std::string& the name of the virtual cluster which got matched. */ - virtual const absl::optional& virtualClusterName() const PURE; + virtual const std::optional& virtualClusterName() const PURE; /** * @param bytes_received denotes number of bytes to add to total received bytes. @@ -800,7 +820,7 @@ class StreamInfo { /** * @return the protocol of the request. */ - virtual absl::optional protocol() const PURE; + virtual std::optional protocol() const PURE; /** * @param protocol the request's protocol. @@ -810,17 +830,17 @@ class StreamInfo { /** * @return the response code. */ - virtual absl::optional responseCode() const PURE; + virtual std::optional responseCode() const PURE; /** * @return the response code details. */ - virtual const absl::optional& responseCodeDetails() const PURE; + virtual const std::optional& responseCodeDetails() const PURE; /** * @return the termination details of the connection. */ - virtual const absl::optional& connectionTerminationDetails() const PURE; + virtual const std::optional& connectionTerminationDetails() const PURE; /** * @return the time that the first byte of the request was received. @@ -852,13 +872,13 @@ class StreamInfo { /** * @return the current duration of the request, or the total duration of the request, if ended. */ - virtual absl::optional currentDuration() const PURE; + virtual std::optional currentDuration() const PURE; /** * @return the total duration of the request (i.e., when the request's ActiveStream is destroyed) * and may be longer than lastDownstreamTxByteSent. */ - virtual absl::optional requestComplete() const PURE; + virtual std::optional requestComplete() const PURE; /** * Sets the end time for the request. This method is called once the request has been fully @@ -1029,10 +1049,10 @@ class StreamInfo { virtual void setAttemptCount(uint32_t attempt_count) PURE; /** - * @return the number of times the request was attempted upstream, absl::nullopt if the request + * @return the number of times the request was attempted upstream, std::nullopt if the request * was never attempted upstream. */ - virtual absl::optional attemptCount() const PURE; + virtual std::optional attemptCount() const PURE; /** * @return the bytes meter for upstream http stream. @@ -1154,12 +1174,12 @@ class StreamInfo { * This should be implemented to call the codecStreamId() method on the * associated Http::Stream object. */ - virtual absl::optional codecStreamId() const PURE; + virtual std::optional codecStreamId() const PURE; /** * @param id the codec level stream ID for the associated stream. */ - virtual void setCodecStreamId(absl::optional id) PURE; + virtual void setCodecStreamId(std::optional id) PURE; }; // An enum representation of the Proxy-Status error space. diff --git a/envoy/thread/thread.h b/envoy/thread/thread.h index 0093a7b520d58..63b914049e02c 100644 --- a/envoy/thread/thread.h +++ b/envoy/thread/thread.h @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include #include "envoy/common/pure.h" @@ -10,7 +12,6 @@ #include "source/common/common/thread_annotations.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Thread { @@ -64,10 +65,14 @@ struct Options { // // If no value is set, the thread will be created with the default thread priority for the // platform. - absl::optional priority_{absl::nullopt}; + std::optional priority_{std::nullopt}; + + // An optional CPU index to pin the thread to. When set, the thread sets its affinity to this + // single CPU at start. Supported on Linux, ignored on other platforms. + std::optional cpu_affinity_{std::nullopt}; }; -using OptionsOptConstRef = const absl::optional&; +using OptionsOptConstRef = const std::optional&; /** * Interface providing a mechanism for creating threads. @@ -83,7 +88,7 @@ class ThreadFactory { * @param options supplies options specified on thread creation. */ virtual ThreadPtr createThread(std::function thread_routine, - OptionsOptConstRef options = absl::nullopt) PURE; + OptionsOptConstRef options = std::nullopt) PURE; /** * Return the current system thread ID diff --git a/envoy/tracing/trace_context.h b/envoy/tracing/trace_context.h index ebf5572c5da62..ed8bb313af8a4 100644 --- a/envoy/tracing/trace_context.h +++ b/envoy/tracing/trace_context.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/optref.h" @@ -8,7 +9,6 @@ #include "envoy/http/header_map.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Tracing { @@ -70,7 +70,7 @@ class TraceContext { * @param key The context key of string view type. * @return The optional context value of string_view type. */ - virtual absl::optional get(absl::string_view key) const PURE; + virtual std::optional get(absl::string_view key) const PURE; /** * Set new tracing context key/value pair. diff --git a/envoy/tracing/trace_driver.h b/envoy/tracing/trace_driver.h index 4e5e9617c6ae3..9aae00d917a00 100644 --- a/envoy/tracing/trace_driver.h +++ b/envoy/tracing/trace_driver.h @@ -113,6 +113,16 @@ class Span { */ virtual void setSampled(bool sampled) PURE; + /** + * @return whether this span will be exported to the tracing backend. The HTTP connection + * manager may skip finalize-time tag work for spans that return false, since those tags + * would be discarded by the driver anyway. + * + * If the driver cannot conclusively determine that the span will be dropped, it MUST + * return true so that the span is fully populated and suitable for export. + */ + virtual bool exportedSpan() const PURE; + /** * When the startSpan() of tracer is called, the Envoy tracing decision is passed to the * tracer to help determine whether the span should be sampled. @@ -130,6 +140,14 @@ class Span { */ virtual bool useLocalDecision() const PURE; + /** + * Stop using the Envoy local tracing decision for this span. After this is called the connection + * manager will not re-derive the sampling decision on a later route refresh, so a decision set + * via setSampled is kept. The default implementation is a no-op and is overridden by tracers that + * support this. + */ + virtual void disableLocalDecision() {} + /** * Retrieve a key's value from the span's baggage. * This baggage data could've been set by this span or any parent spans. diff --git a/envoy/udp/BUILD b/envoy/udp/BUILD index 4ec0a65032a94..1318960a3d11b 100644 --- a/envoy/udp/BUILD +++ b/envoy/udp/BUILD @@ -13,6 +13,5 @@ envoy_cc_library( hdrs = ["hash_policy.h"], deps = [ "//envoy/network:address_interface", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/envoy/udp/hash_policy.h b/envoy/udp/hash_policy.h index bbe9d79b585e4..ab7416c65f786 100644 --- a/envoy/udp/hash_policy.h +++ b/envoy/udp/hash_policy.h @@ -1,8 +1,8 @@ #pragma once -#include "envoy/network/address.h" +#include -#include "absl/types/optional.h" +#include "envoy/network/address.h" namespace Envoy { namespace Udp { @@ -15,10 +15,10 @@ class HashPolicy { /** * @param downstream_address is the address of the peer client. - * @return absl::optional an optional hash value to route on. A hash value might not be + * @return std::optional an optional hash value to route on. A hash value might not be * returned if for example the downstream address has a unix domain socket type. */ - virtual absl::optional + virtual std::optional generateHash(const Network::Address::Instance& downstream_address) const PURE; }; } // namespace Udp diff --git a/envoy/upstream/BUILD b/envoy/upstream/BUILD index 5bd0d1449035b..ad554032e881a 100644 --- a/envoy/upstream/BUILD +++ b/envoy/upstream/BUILD @@ -30,6 +30,8 @@ envoy_cc_library( "//envoy/server:admin_interface", "//envoy/server:options_interface", "//envoy/singleton:manager_interface", + "//envoy/stats:stats_interface", + "//envoy/stats:store_interface", "//envoy/tcp:conn_pool_interface", "//envoy/thread_local:thread_local_interface", "@abseil-cpp//absl/container:node_hash_map", @@ -111,7 +113,6 @@ envoy_cc_library( hdrs = ["outlier_detection.h"], deps = [ "//envoy/common:time_interface", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/data/cluster/v3:pkg_cc_proto", ], ) @@ -171,7 +172,6 @@ envoy_cc_library( "//envoy/ssl:context_interface", "//envoy/ssl:context_manager_interface", "//envoy/upstream:types_interface", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/envoy/upstream/cluster_manager.h b/envoy/upstream/cluster_manager.h index 72af71f9085e3..62c0993609f0e 100644 --- a/envoy/upstream/cluster_manager.h +++ b/envoy/upstream/cluster_manager.h @@ -26,6 +26,7 @@ #include "envoy/server/options.h" #include "envoy/singleton/manager.h" #include "envoy/ssl/context_manager.h" +#include "envoy/stats/scope.h" #include "envoy/stats/store.h" #include "envoy/tcp/conn_pool.h" #include "envoy/thread_local/thread_local.h" @@ -77,7 +78,7 @@ class ClusterUpdateCallbacks { * onClusterRemoval is called when a cluster is removed; the argument is the cluster name. * @param cluster_name is the name of the removed cluster. */ - virtual void onClusterRemoval(const std::string& cluster_name) PURE; + virtual void onClusterRemoval(absl::string_view cluster_name) PURE; }; /** @@ -286,7 +287,7 @@ class ClusterManager { */ virtual absl::StatusOr addOrUpdateCluster(const envoy::config::cluster::v3::Cluster& cluster, - const std::string& version_info, const bool avoid_cds_removal = false) PURE; + absl::string_view version_info, const bool avoid_cds_removal = false) PURE; /** * Set a callback that will be invoked when all primary clusters have been initialized. @@ -322,7 +323,7 @@ class ClusterManager { if (warming_cluster != warming_clusters_.cend()) { return warming_cluster->second; } - return absl::nullopt; + return std::nullopt; } ClusterInfoMap active_clusters_; @@ -355,7 +356,7 @@ class ClusterManager { * * NOTE: This method is only thread safe on the main thread. It should not be called elsewhere. */ - virtual OptRef getActiveCluster(const std::string& cluster_name) const PURE; + virtual OptRef getActiveCluster(absl::string_view cluster_name) const PURE; /** * Receives a cluster name and returns an active or warming cluster (if found). @@ -365,7 +366,7 @@ class ClusterManager { * NOTE: This method is only thread safe on the main thread. It should not be called elsewhere. */ virtual OptRef - getActiveOrWarmingCluster(const std::string& cluster_name) const PURE; + getActiveOrWarmingCluster(absl::string_view cluster_name) const PURE; /** * Returns true iff the given cluster name is known in the cluster-manager @@ -375,7 +376,7 @@ class ClusterManager { * * NOTE: This method is only thread safe on the main thread. It should not be called elsewhere. */ - virtual bool hasCluster(const std::string& cluster_name) const PURE; + virtual bool hasCluster(absl::string_view cluster_name) const PURE; /** * Returns true iff there's an active cluster in the cluster-manager. @@ -418,7 +419,7 @@ class ClusterManager { * can be removed by setting `remove_ignored` to true while removeCluster(). * @return true if the action results in the removal of a cluster. */ - virtual bool removeCluster(const std::string& cluster, const bool remove_ignored = false) PURE; + virtual bool removeCluster(absl::string_view cluster, const bool remove_ignored = false) PURE; /** * Shutdown the cluster manager prior to destroying connection pools and other thread local data. @@ -433,7 +434,7 @@ class ClusterManager { /** * @return cluster manager wide bind configuration for new upstream connections. */ - virtual const absl::optional& bindConfig() const PURE; + virtual const std::optional& bindConfig() const PURE; /** * Returns a shared_ptr to the singleton xDS-over-gRPC provider for upstream control plane muxing @@ -453,10 +454,10 @@ class ClusterManager { /** * Return the local cluster name, if it was configured. * - * @return absl::optional the local cluster name, or empty if no local cluster was + * @return std::optional the local cluster name, or empty if no local cluster was * configured. */ - virtual const absl::optional& localClusterName() const PURE; + virtual const std::optional& localClusterName() const PURE; /** * This method allows to register callbacks for cluster lifecycle events in the ClusterManager. @@ -506,6 +507,7 @@ class ClusterManager { * to all worker threads and run concurrently. */ using DrainConnectionsHostPredicate = std::function; + using DrainConnectionsPoolPredicate = std::function; /** * Drain all connection pool connections owned by this cluster. @@ -513,7 +515,7 @@ class ClusterManager { * @param predicate supplies the optional drain connections host predicate. If not supplied, all * hosts are drained. */ - virtual void drainConnections(const std::string& cluster, + virtual void drainConnections(absl::string_view cluster, DrainConnectionsHostPredicate predicate) PURE; /** @@ -524,11 +526,19 @@ class ClusterManager { virtual void drainConnections(DrainConnectionsHostPredicate predicate, ConnectionPool::DrainBehavior drain_behavior) PURE; + /** + * Drain all connection pool connections matching the pool predicate owned by + * all clusters. + * @param predicate supplies the drain connections pool predicate. + */ + virtual void drainOrCloseConnPools(DrainConnectionsPoolPredicate predicate, + ConnectionPool::DrainBehavior drain_behavior) PURE; + /** * Check if the cluster is active and statically configured, and if not, return an error * @param cluster, the cluster to check. */ - virtual absl::Status checkActiveStaticCluster(const std::string& cluster) PURE; + virtual absl::Status checkActiveStaticCluster(absl::string_view cluster) PURE; /** * Allocates an on-demand CDS API provider from configuration proto or locator. @@ -627,7 +637,7 @@ class ClusterManagerFactory { virtual Http::ConnectionPool::InstancePtr allocateConnPool(Event::Dispatcher& dispatcher, HostConstSharedPtr host, ResourcePriority priority, std::vector& protocol, - const absl::optional& + const std::optional& alternate_protocol_options, const Network::ConnectionSocket::OptionsSharedPtr& options, const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, @@ -645,7 +655,7 @@ class ClusterManagerFactory { const Network::ConnectionSocket::OptionsSharedPtr& options, Network::TransportSocketOptionsConstSharedPtr transport_socket_options, ClusterConnectivityState& state, - absl::optional tcp_pool_idle_timeout) PURE; + std::optional tcp_pool_idle_timeout) PURE; /** * Allocate a cluster from configuration proto. diff --git a/envoy/upstream/health_checker.h b/envoy/upstream/health_checker.h index ed6db4abe84a2..5cb4f354fba5c 100644 --- a/envoy/upstream/health_checker.h +++ b/envoy/upstream/health_checker.h @@ -74,10 +74,13 @@ class HealthCheckEventLogger { * @param health_checker_type supplies the type of health checker that generated the event. * @param host supplies the host that generated the event. * @param failure_type supplies the type of health check failure. + * @param http_status_code the HTTP status code of the response that triggered the ejection, + * or 0 if not applicable (e.g., non-HTTP checkers or network-level failures). */ virtual void logEjectUnhealthy(envoy::data::core::v3::HealthCheckerType health_checker_type, const HostDescriptionConstSharedPtr& host, - envoy::data::core::v3::HealthCheckFailureType failure_type) PURE; + envoy::data::core::v3::HealthCheckFailureType failure_type, + uint64_t http_status_code) PURE; /** * Log an unhealthy host event. @@ -85,11 +88,13 @@ class HealthCheckEventLogger { * @param host supplies the host that generated the event. * @param failure_type supplies the type of health check failure. * @param first_check whether this is a failure on the first health check for this host. + * @param http_status_code the HTTP status code of the response that caused this failure, + * or 0 if not applicable (e.g., non-HTTP checkers or network-level failures). */ virtual void logUnhealthy(envoy::data::core::v3::HealthCheckerType health_checker_type, const HostDescriptionConstSharedPtr& host, envoy::data::core::v3::HealthCheckFailureType failure_type, - bool first_check) PURE; + bool first_check, uint64_t http_status_code) PURE; /** * Log a healthy host addition event. diff --git a/envoy/upstream/host_description.h b/envoy/upstream/host_description.h index 434f326505d45..c686b13227b0a 100644 --- a/envoy/upstream/host_description.h +++ b/envoy/upstream/host_description.h @@ -45,7 +45,8 @@ using MetadataConstSharedPtr = std::shared_ptr`. If this method + * returns an empty string view, then the host's address should be used as fallback for the + * observability name. + */ + virtual absl::string_view observabilityName() const PURE; + /** * @return the transport socket factory responsible for this host. */ @@ -263,6 +273,12 @@ class HostDescription { */ virtual Network::Address::InstanceConstSharedPtr healthCheckAddress() const PURE; + /** + * @return the address used to dial ORCA out-of-band load reporting streams + * (xds.service.orca.v3.OpenRcaService). + */ + virtual Network::Address::InstanceConstSharedPtr orcaReportingAddress() const PURE; + /** * @return the priority of the host. */ @@ -277,7 +293,7 @@ class HostDescription { * @return timestamp of when host has transitioned from unhealthy to * healthy state via an active healthchecking. */ - virtual absl::optional lastHcPassTime() const PURE; + virtual std::optional lastHcPassTime() const PURE; /** * Set the timestamp of when the host has transitioned from unhealthy to healthy state via an diff --git a/envoy/upstream/load_balancer.h b/envoy/upstream/load_balancer.h index 3ef70a0c09fbb..43e4fc4b1528b 100644 --- a/envoy/upstream/load_balancer.h +++ b/envoy/upstream/load_balancer.h @@ -6,6 +6,7 @@ #include "envoy/common/optref.h" #include "envoy/common/pure.h" #include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/http/codes.h" #include "envoy/network/transport_socket.h" #include "envoy/router/router.h" #include "envoy/stream_info/stream_info.h" @@ -30,14 +31,14 @@ namespace Upstream { using ClusterProto = envoy::config::cluster::v3::Cluster; /* - * A handle to allow cancelation of asynchronous host selection. + * A handle to allow cancellation of asynchronous host selection. * If chooseHost returns a HostSelectionResponse with an AsyncHostSelectionHandle - * handle, and the endpoint does not wish to receive onAsyncHostSelction call, + * handle, and the endpoint does not wish to receive onAsyncHostSelection call, * it must call cancel() on the provided handle. * * Please note that the AsyncHostSelectionHandle may be deleted after the - * cancel() call. It is up to the implemention of the asynchronous load balancer - * to ensure the cancelation state persists until the load balancer checks it. + * cancel() call. It is up to the implementation of the asynchronous load balancer + * to ensure the cancellation state persists until the load balancer checks it. */ class AsyncHostSelectionHandle { public: @@ -53,7 +54,7 @@ class AsyncHostSelectionHandle { * load balancing, returns an AsyncHostSelectionHandle handle. * * If it returns a AsyncHostSelectionHandle handle, the load balancer guarantees an - * eventual call to LoadBalancerContext::onAsyncHostSelction unless + * eventual call to LoadBalancerContext::onAsyncHostSelection unless * AsyncHostSelectionHandle::cancel is called. */ struct HostSelectionResponse { @@ -66,6 +67,9 @@ struct HostSelectionResponse { // Optional details if host selection fails (empty string implies no details). std::string details; std::unique_ptr cancelable; + // Optional HTTP status code to use when host selection fails because the strict override + // destination is missing from available endpoints. If not set, defaults to 503. + std::optional failure_status; }; /** @@ -80,9 +84,9 @@ class LoadBalancerContext { * Compute and return an optional hash key to use during load balancing. This * method may modify internal state so it should only be called once per * routing attempt. - * @return absl::optional the optional hash key to use. + * @return std::optional the optional hash key to use. */ - virtual absl::optional computeHashKey() PURE; + virtual std::optional computeHashKey() PURE; /** * @return Router::MetadataMatchCriteria* metadata for use in selecting a subset of hosts @@ -156,6 +160,10 @@ class LoadBalancerContext { // If strict and no valid host is found, the load balancer should return nullptr. // If not strict, the load balancer will select another host if the target host is not valid. bool strict{false}; + // The HTTP status code to return when strict mode is enabled and the target host + // is not found in the set of available endpoints. Does not apply when the host + // exists but is unhealthy. Defaults to 503 (ServiceUnavailable). + Http::Code status_on_strict_destination_not_found{Http::Code::ServiceUnavailable}; }; /** * Returns the host the load balancer should select directly. If the expected host exists and @@ -246,7 +254,7 @@ class LoadBalancer { * @return selected pool and connection to be used, or nullopt if no selection is made, * for example if no matching connection is found. */ - virtual absl::optional + virtual std::optional selectExistingConnection(LoadBalancerContext* context, const Host& host, std::vector& hash_key) PURE; }; @@ -276,9 +284,17 @@ class LoadBalancerFactory { virtual LoadBalancerPtr create(LoadBalancerParams params) PURE; /** - * @return bool whether the load balancer should be recreated when the host set changes. + * @return bool whether the cluster manager should recreate this worker-local load balancer + * every time the host set changes. + * + * DEPRECATED: this hook exists only to keep not-yet-migrated load balancers working while the + * cluster manager's recreate-on-host-change loop is phased out. New load balancers MUST return + * false and refresh any host-derived state by subscribing to the priority set via + * PrioritySet::addMemberUpdateCb. The hook has no default so out-of-tree implementations are + * forced to make an explicit choice during the migration; it will be removed once all known + * implementations have moved off the legacy contract. */ - virtual bool recreateOnHostChange() const { return true; } + virtual bool recreateOnHostChangeDeprecated() const PURE; }; using LoadBalancerFactorySharedPtr = std::shared_ptr; diff --git a/envoy/upstream/outlier_detection.h b/envoy/upstream/outlier_detection.h index f7cc0ad110cc2..aa017ade98fa2 100644 --- a/envoy/upstream/outlier_detection.h +++ b/envoy/upstream/outlier_detection.h @@ -4,13 +4,12 @@ #include #include #include +#include #include "envoy/common/pure.h" #include "envoy/common/time.h" #include "envoy/data/cluster/v3/outlier_detection_event.pb.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Upstream { @@ -68,13 +67,13 @@ class DetectorHostMonitor { * Some non-HTTP codes like TIMEOUT may require special mapping to HTTP code * and such code may be passed as optional parameter. */ - virtual void putResult(Result result, absl::optional code) PURE; + virtual void putResult(Result result, std::optional code) PURE; /** * Wrapper around putResult with 2 params when mapping to HTTP code is not * required. */ - void putResult(Result result) { putResult(result, absl::nullopt); } + void putResult(Result result) { putResult(result, std::nullopt); } /** * Add a response time for a host (in this case response time is generic and might be used for @@ -86,13 +85,13 @@ class DetectorHostMonitor { * Get the time of last ejection. * @return the last time this host was ejected, if the host has been ejected previously. */ - virtual const absl::optional& lastEjectionTime() PURE; + virtual const std::optional& lastEjectionTime() PURE; /** * Get the time of last unejection. * @return the last time this host was unejected, if the host has been unejected previously. */ - virtual const absl::optional& lastUnejectionTime() PURE; + virtual const std::optional& lastUnejectionTime() PURE; /** * @return the success rate of the host in the last calculated interval, in the range 0-100. diff --git a/envoy/upstream/retry.h b/envoy/upstream/retry.h index 6b81aa25dc910..1bdaeb32c08af 100644 --- a/envoy/upstream/retry.h +++ b/envoy/upstream/retry.h @@ -24,14 +24,14 @@ class RetryPriority { * Function that maps a HostDescription to it's effective priority level in a cluster. * For most cluster types, the mapping is simply `return host.priority()`, but some * cluster types require more complex mapping. - * @return either the effective priority, or absl::nullopt if the mapping cannot be determined, + * @return either the effective priority, or std::nullopt if the mapping cannot be determined, * which can happen if the host has been removed from the configurations since it was * used. */ using PriorityMappingFunc = - std::function(const Upstream::HostDescription&)>; + std::function(const Upstream::HostDescription&)>; - static absl::optional defaultPriorityMapping(const Upstream::HostDescription& host) { + static std::optional defaultPriorityMapping(const Upstream::HostDescription& host) { return host.priority(); } @@ -113,7 +113,7 @@ class RetryOptionsPredicate { struct UpdateOptionsReturn { // New upstream socket options to apply to the next request attempt. If changed, will affect // connection pool selection similar to that which was done for the initial request. - absl::optional new_upstream_socket_options_; + std::optional new_upstream_socket_options_; }; virtual ~RetryOptionsPredicate() = default; diff --git a/envoy/upstream/thread_local_cluster.h b/envoy/upstream/thread_local_cluster.h index 5635169f52e81..bd28ce36369d8 100644 --- a/envoy/upstream/thread_local_cluster.h +++ b/envoy/upstream/thread_local_cluster.h @@ -118,9 +118,9 @@ class ThreadLocalCluster { * valid until newConnection is called on the pool (if it is to be called). * @return the connection pool data or nullopt if there is no host available in the cluster. */ - virtual absl::optional + virtual std::optional httpConnPool(HostConstSharedPtr host, ResourcePriority priority, - absl::optional downstream_protocol, + std::optional downstream_protocol, LoadBalancerContext* context) PURE; /** @@ -133,13 +133,12 @@ class ThreadLocalCluster { * valid until newConnection is called on the pool (if it is to be called). * @return the connection pool data or nullopt if there is no host available in the cluster. */ - virtual absl::optional tcpConnPool(HostConstSharedPtr host, - ResourcePriority priority, - LoadBalancerContext* context) PURE; + virtual std::optional tcpConnPool(HostConstSharedPtr host, ResourcePriority priority, + LoadBalancerContext* context) PURE; /* a legacy API which synchronously chooses a host and creates a conn pool*/ - virtual absl::optional tcpConnPool(ResourcePriority priority, - LoadBalancerContext* context) PURE; + virtual std::optional tcpConnPool(ResourcePriority priority, + LoadBalancerContext* context) PURE; /** * Allocate a load balanced TCP connection for a cluster. The created connection is already @@ -189,7 +188,7 @@ class ThreadLocalCluster { virtual void setDropCategory(absl::string_view drop_category) PURE; }; -using ThreadLocalClusterOptRef = absl::optional>; +using ThreadLocalClusterOptRef = std::optional>; } // namespace Upstream } // namespace Envoy diff --git a/envoy/upstream/upstream.h b/envoy/upstream/upstream.h index 99fdecb7ebf58..ac43b2a98f024 100644 --- a/envoy/upstream/upstream.h +++ b/envoy/upstream/upstream.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -29,11 +30,11 @@ #include "envoy/upstream/types.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" #include "fmt/format.h" namespace Envoy { namespace Http { +class ClientCodecFactory; class FilterChainManager; class HashPolicy; } // namespace Http @@ -121,7 +122,7 @@ class UpstreamLocalAddressSelectorFactory : public Config::TypedFactory { */ virtual absl::StatusOr createLocalAddressSelector(std::vector upstream_local_addresses, - absl::optional cluster_name) const PURE; + std::optional cluster_name) const PURE; std::string category() const override { return "envoy.upstream.local_address_selector"; } }; @@ -216,6 +217,23 @@ class Host : virtual public HostDescription { Network::TransportSocketOptionsConstSharedPtr transport_socket_options, const envoy::config::core::v3::Metadata* metadata) const PURE; + /** + * Create a dedicated connection for ORCA out-of-band load reporting per gRFC A51 + * (xds.service.orca.v3.OpenRcaService), separate from request and health-check pools. + * @param dispatcher supplies the owning dispatcher. + * @param transport_socket_options supplies the transport options that will be set on the new + * connection. + * @param factory the transport socket factory for the connection, resolved by the caller. + * @param orca_address the resolved address to dial; the caller applies any port override. + * Implementations should use the host's address list only when this equals the host address. + * @return the connection data. + */ + virtual CreateConnectionData createOrcaReportingConnection( + Event::Dispatcher& dispatcher, + Network::TransportSocketOptionsConstSharedPtr transport_socket_options, + Network::UpstreamTransportSocketFactory& factory, + Network::Address::InstanceConstSharedPtr orca_address) const PURE; + /** * @return host specific gauges. */ @@ -335,7 +353,7 @@ class Host : virtual public HostDescription { * @return the HTTP status code from the last active health check response, or * 0 if no response has been recorded. */ - virtual absl::optional lastHealthCheckHttpStatus() const PURE; + virtual std::optional lastHealthCheckHttpStatus() const PURE; }; using HostConstSharedPtr = std::shared_ptr; @@ -610,8 +628,8 @@ class PrioritySet { virtual void updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - absl::optional weighted_priority_health, - absl::optional overprovisioning_factor, + std::optional weighted_priority_health, + std::optional overprovisioning_factor, HostMapConstSharedPtr cross_priority_host_map = nullptr) PURE; /** @@ -634,8 +652,8 @@ class PrioritySet { virtual void updateHosts(uint32_t priority, UpdateHostsParams&& update_hosts_params, LocalityWeightsConstSharedPtr locality_weights, const HostVector& hosts_added, const HostVector& hosts_removed, - absl::optional weighted_priority_health, - absl::optional overprovisioning_factor) PURE; + std::optional weighted_priority_health, + std::optional overprovisioning_factor) PURE; }; /** @@ -751,6 +769,7 @@ class PrioritySet { COUNTER(upstream_rq_completed) \ COUNTER(upstream_rq_maintenance_mode) \ COUNTER(upstream_rq_max_duration_reached) \ + COUNTER(upstream_rq_active_overflow) \ COUNTER(upstream_rq_pending_failure_eject) \ COUNTER(upstream_rq_pending_overflow) \ COUNTER(upstream_rq_pending_total) \ @@ -764,6 +783,7 @@ class PrioritySet { COUNTER(upstream_rq_retry_overflow) \ COUNTER(upstream_rq_retry_success) \ COUNTER(upstream_rq_rx_reset) \ + COUNTER(upstream_rq_rx_reset_no_error) \ COUNTER(upstream_rq_timeout) \ COUNTER(upstream_rq_total) \ COUNTER(upstream_rq_tx_reset) \ @@ -885,11 +905,11 @@ struct ClusterCircuitBreakersStats { using ClusterRequestResponseSizeStatsPtr = std::unique_ptr; using ClusterRequestResponseSizeStatsOptRef = - absl::optional>; + std::optional>; using ClusterTimeoutBudgetStatsPtr = std::unique_ptr; using ClusterTimeoutBudgetStatsOptRef = - absl::optional>; + std::optional>; /** * All extension protocol specific options returned by the method at @@ -899,6 +919,18 @@ using ClusterTimeoutBudgetStatsOptRef = class ProtocolOptionsConfig { public: virtual ~ProtocolOptionsConfig() = default; + + /** + * @return an optional upstream (client) HTTP codec factory provided by this options object. + * Defaults to none. An options extension attached via typed_extension_protocol_options + * can override this to make CodecClientProd build a custom or decorated client codec. + * The returned factory's lifetime must be owned by this options object (e.g. by being + * the object itself or a member of it): ClusterInfoImpl pins it with a shared_ptr + * aliasing the options object, so a factory owned elsewhere would dangle. + */ + virtual OptRef upstreamHttpClientCodecFactory() const { + return {}; + } }; using ProtocolOptionsConfigConstSharedPtr = std::shared_ptr; @@ -934,19 +966,19 @@ class HttpProtocolOptionsConfig : public ProtocolOptionsConfig { commonHttpProtocolOptions() const PURE; /** - * @return const absl::optional& the - * optional upstream-specific HTTP protocol options. Returns absl::nullopt if not + * @return const std::optional& the + * optional upstream-specific HTTP protocol options. Returns std::nullopt if not * configured. */ - virtual const absl::optional& + virtual const std::optional& upstreamHttpProtocolOptions() const PURE; /** - * @return const absl::optional& + * @return const std::optional& * the optional alternate protocols cache options for upstream connections. Returns - * absl::nullopt if not configured. + * std::nullopt if not configured. */ - virtual const absl::optional& + virtual const std::optional& alternateProtocolsCacheOptions() const PURE; /** @@ -1023,17 +1055,17 @@ class ClusterInfo : public Http::FilterChainFactory { /** * @return the idle timeout for upstream HTTP connection pool connections. */ - virtual const absl::optional idleTimeout() const PURE; + virtual const std::optional idleTimeout() const PURE; /** * @return the idle timeout for each connection in TCP connection pool. */ - virtual const absl::optional tcpPoolIdleTimeout() const PURE; + virtual const std::optional tcpPoolIdleTimeout() const PURE; /** * @return optional maximum connection duration timeout for manager connections. */ - virtual const absl::optional maxConnectionDuration() const PURE; + virtual const std::optional maxConnectionDuration() const PURE; /** * @return how many streams should be anticipated per each current stream. @@ -1077,6 +1109,15 @@ class ClusterInfo : public Http::FilterChainFactory { return std::dynamic_pointer_cast(extensionProtocolOptions(name)); } + /** + * @return OptRef a per-cluster factory for the upstream (client) + * HTTP codec, if one was attached via typed_extension_protocol_options. When empty, the + * stock codec is used. Non-pure so that only ClusterInfoImpl has to provide it. + */ + virtual OptRef upstreamHttpClientCodecFactory() const { + return {}; + } + /** * @return OptRef the validated load balancing policy configuration to * use for this cluster. @@ -1097,11 +1138,11 @@ class ClusterInfo : public Http::FilterChainFactory { /** * @param response Http::ResponseHeaderMap response headers received from upstream - * @return absl::optional absl::nullopt is returned when matching did not took place. + * @return std::optional std::nullopt is returned when matching did not took place. * Otherwise, the boolean value indicates the matching result. True indicates that * response should be treated as error, False as success. */ - virtual absl::optional + virtual std::optional processHttpForOutlierDetection(Http::ResponseHeaderMap& response) const PURE; /** @@ -1116,7 +1157,7 @@ class ClusterInfo : public Http::FilterChainFactory { clusterType() const PURE; /** - * @return const absl::optional& the configuration + * @return const std::optional& the configuration * for the upstream, if a custom upstream is configured. */ virtual OptRef upstreamConfig() const PURE; @@ -1145,7 +1186,7 @@ class ClusterInfo : public Http::FilterChainFactory { /** * @return uint32_t the maximum total size of response headers in KB. */ - virtual absl::optional maxResponseHeadersKb() const PURE; + virtual std::optional maxResponseHeadersKb() const PURE; /** * @return the human readable name of the cluster. @@ -1203,13 +1244,13 @@ class ClusterInfo : public Http::FilterChainFactory { virtual ClusterLoadReportStats& loadReportStats() const PURE; /** - * @return absl::optional> stats to track + * @return std::optional> stats to track * headers/body sizes of request/response for this cluster. */ virtual ClusterRequestResponseSizeStatsOptRef requestResponseSizeStats() const PURE; /** - * @return absl::optional> stats on timeout + * @return std::optional> stats on timeout * budgets for this cluster. */ virtual ClusterTimeoutBudgetStatsOptRef timeoutBudgetStats() const PURE; @@ -1272,7 +1313,7 @@ class ClusterInfo : public Http::FilterChainFactory { * Calculate upstream protocol(s) based on features. */ virtual std::vector - upstreamHttpProtocol(absl::optional downstream_protocol) const PURE; + upstreamHttpProtocol(std::optional downstream_protocol) const PURE; /** * @return the Http1 Codec Stats. @@ -1399,7 +1440,7 @@ class Cluster { }; using ClusterSharedPtr = std::shared_ptr; -using ClusterConstOptRef = absl::optional>; +using ClusterConstOptRef = std::optional>; } // namespace Upstream } // namespace Envoy diff --git a/go.mod b/go.mod index ef30f56402dc9..d779746353bd8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/envoyproxy/envoy -go 1.24.6 +go 1.25.0 require ( github.com/envoyproxy/go-control-plane/envoy v1.37.0 @@ -12,9 +12,9 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/prometheus/client_model v0.6.2 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/grpc v1.79.3 // indirect diff --git a/go.sum b/go.sum index cb9130f2ad13d..1c973036f9579 100644 --- a/go.sum +++ b/go.sum @@ -32,12 +32,12 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= diff --git a/mobile/.bazelrc b/mobile/.bazelrc index 5fac811b42760..c15ad4112d5de 100644 --- a/mobile/.bazelrc +++ b/mobile/.bazelrc @@ -36,9 +36,6 @@ build --define=envoy_full_protos=disabled build --define envoy_exceptions=disabled build: --copt=-fno-exceptions -# We don't have a ton of Swift in Envoy Mobile, so always build with WMO -# This also helps work around a bug in rules_swift: https://github.com/bazelbuild/rules_swift/issues/949 -build --swiftcopt=-wmo # https://github.com/bazelbuild/rules_jvm_external/issues/445 build --repo_env=JAVA_HOME=../bazel_tools/jdk build --define disable_known_issue_asserts=true @@ -75,12 +72,13 @@ build:ios --define=envoy_full_protos=enabled build:android --define=logger=android build:android --java_language_version=8 build:android --tool_java_language_version=8 +build:android --cxxopt=-Wno-nullability-completeness # Default flags for Android -build:mobile-android --incompatible_enable_android_toolchain_resolution build:mobile-android --config=mobile-clang build:mobile-android --java_language_version=8 build:mobile-android --tool_java_language_version=8 +build:mobile-android --cxxopt=-Wno-nullability-completeness test:mobile-android --build_tests_only test:mobile-android --define=static_extension_registration=disabled @@ -91,12 +89,10 @@ build:mobile-android-release --config=mobile-release-common build:mobile-android-release --compilation_mode=opt build:mobile-android-release --linkopt=-fuse-ld=lld build:mobile-android-release --android_platforms=@envoy//bazel/platforms/android:x86_64 -build:mobile-android-release --fat_apk_cpu=x86_64 build:mobile-android-publish --config=mobile-android-release build:mobile-android-publish --config=mobile-release build:mobile-android-publish --android_platforms=@envoy//bazel/platforms/android:x86,@envoy//bazel/platforms/android:x86_64,@envoy//bazel/platforms/android:armeabi-v7a,@envoy//bazel/platforms/android::arm64-v8a -build:mobile-android-publish --fat_apk_cpu=x86,x86_64,armeabi-v7a,arm64-v8a ############################################################################# @@ -114,6 +110,7 @@ build:mobile-clang --host_platform=@mobile_clang_platform build:mobile-rbe --google_default_credentials=false build:mobile-rbe --remote_cache=grpcs://envoy.cluster.engflow.com build:mobile-rbe --remote_executor=grpcs://envoy.cluster.engflow.com +build:mobile-rbe --experimental_remote_downloader=grpcs://envoy.cluster.engflow.com build:mobile-rbe --bes_backend=grpcs://envoy.cluster.engflow.com/ build:mobile-rbe --bes_results_url=https://envoy.cluster.engflow.com/invocation/ build:mobile-rbe --credential_helper=*.engflow.com=%workspace%/bazel/engflow-bazel-credential-helper.sh @@ -128,6 +125,10 @@ build:mobile-rbe --verbose_failures build:mobile-rbe --spawn_strategy=remote,sandboxed,local build:mobile-rbe --experimental_ui_max_stdouterr_bytes=10485760 build:mobile-rbe --extra_execution_platforms=//bazel/platforms/rbe:linux_x64 +# Recover from transient gRPC failures (e.g. UNAVAILABLE: io exception) talking to engflow. +build:mobile-rbe --remote_retries=10 +build:mobile-rbe --remote_retry_max_delay=60s +build:mobile-rbe --experimental_remote_cache_eviction_retries=5 ############################################################################# @@ -149,6 +150,7 @@ build:mobile-tsan --config=tsan build:mobile-tsan --config=mobile-clang build:mobile-tsan --build_tests_only test:mobile-tsan --test_env=ENVOY_IP_TEST_VERSIONS=v4only +build:mobile-tsan --test_timeout=240,1200,3000,9600 ############################################################################# @@ -162,7 +164,7 @@ build:mobile-cc --test_env=ENVOY_IP_TEST_VERSIONS=v4only # CC with xDS build:mobile-cc-xds-enabled --config=mobile-cc -test:mobile-cc-xds-enabled --define=envoy_mobile_xds=enabled +build:mobile-cc-xds-enabled --define=envoy_mobile_xds=enabled # Coverage build:mobile-coverage --build_tests_only diff --git a/mobile/BUILD b/mobile/BUILD index 8cca5618955bc..d245740c2dbd1 100644 --- a/mobile/BUILD +++ b/mobile/BUILD @@ -58,6 +58,12 @@ aar_import( name = "envoy_mobile_android", aar = "//library/kotlin/io/envoyproxy/envoymobile:envoy_aar", visibility = ["//visibility:public"], + deps = [ + "@maven//:androidx_annotation_annotation", + "@maven//:androidx_core_core", + "@maven//:org_jetbrains_annotations", + "@maven//:org_jetbrains_kotlin_kotlin_stdlib", + ], ) alias( @@ -65,11 +71,6 @@ alias( actual = "//library/kotlin/io/envoyproxy/envoymobile:envoy_aar_with_artifacts", ) -alias( - name = "android_xds_dist", - actual = "//library/kotlin/io/envoyproxy/envoymobile:envoy_xds_aar_with_artifacts", -) - define_kt_toolchain( name = "kotlin_toolchain", jvm_target = "1.8", diff --git a/mobile/MODULE.bazel b/mobile/MODULE.bazel index a1cbd57f26808..410ba70a4422f 100644 --- a/mobile/MODULE.bazel +++ b/mobile/MODULE.bazel @@ -24,7 +24,6 @@ local_path_override( ) pip.parse( - experimental_index_url = "https://us-python.pkg.dev/artifact-foundry-prod/python-3p-trusted/simple/", hub_name = "pypi", python_version = PYTHON_VERSION, requirements_lock = "//:requirements.txt", diff --git a/mobile/WORKSPACE b/mobile/WORKSPACE index d05da15a484d9..88d5928ae40b2 100644 --- a/mobile/WORKSPACE +++ b/mobile/WORKSPACE @@ -61,6 +61,10 @@ load("@envoy//bazel:toolchains.bzl", "envoy_toolchains") envoy_toolchains() +load("@rules_android//:prereqs.bzl", "rules_android_prereqs") + +rules_android_prereqs() + load("@envoy_mobile//bazel:envoy_mobile_dependencies.bzl", "envoy_mobile_dependencies") envoy_mobile_dependencies() @@ -73,11 +77,16 @@ load("@envoy_mobile//bazel:platforms.bzl", "envoy_mobile_platforms") envoy_mobile_platforms() -load("//bazel:python.bzl", "declare_python_abi") +load("@rules_python//python:repositories.bzl", "python_register_toolchains") -declare_python_abi( - name = "python_abi", - python_version = "3", +python_register_toolchains( + name = "python3_13", + python_version = "3.13.0", +) + +python_register_toolchains( + name = "python3_14", + python_version = "3.14.0", ) load("@mobile_pip3//:requirements.bzl", pip_dependencies = "install_deps") @@ -88,7 +97,7 @@ load("//bazel:android_configure.bzl", "android_configure") android_configure( name = "local_config_android", - build_tools_version = "30.0.2", + build_tools_version = "35.0.0", ndk_api_level = 23, sdk_api_level = 30, ) @@ -97,6 +106,9 @@ load("@local_config_android//:android_configure.bzl", "android_workspace") android_workspace() +register_toolchains("@rules_android//toolchains/android:all") +register_toolchains("@rules_android//toolchains/android_sdk:all") + load("@com_github_buildbuddy_io_rules_xcodeproj//xcodeproj:repositories.bzl", "xcodeproj_rules_dependencies") xcodeproj_rules_dependencies() diff --git a/mobile/bazel/android_debug_info.bzl b/mobile/bazel/android_debug_info.bzl index 313e25c177fcb..ab35777937979 100644 --- a/mobile/bazel/android_debug_info.bzl +++ b/mobile/bazel/android_debug_info.bzl @@ -9,6 +9,7 @@ But even if we could create those we'd need to get them out of the build somehow, this rule provides a separate --output_group for this """ +load("@rules_android//rules:android_split_transition.bzl", "android_split_transition") load("@rules_cc//cc/common:cc_common.bzl", "cc_common") load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") @@ -16,13 +17,12 @@ def _impl(ctx): library_outputs = [] objdump_outputs = [] for platform, dep in ctx.split_attr.dep.items(): - # When --fat_apk_cpu isn't set, the platform is None if len(dep.files.to_list()) != 1: fail("Expected exactly one file in the library") cc_toolchain = ctx.split_attr._cc_toolchain[platform][cc_common.CcToolchainInfo] lib = dep.files.to_list()[0] - platform_name = platform or ctx.fragments.android.android_cpu + platform_name = platform or "default" objdump_output = ctx.actions.declare_file(platform_name + "/" + platform_name + ".objdump.gz") ctx.actions.run_shell( @@ -55,11 +55,14 @@ android_debug_info = rule( attrs = dict( dep = attr.label( providers = [CcInfo], - cfg = android_common.multi_cpu_configuration, + cfg = android_split_transition, ), _cc_toolchain = attr.label( - default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), - cfg = android_common.multi_cpu_configuration, + default = Label("@rules_cc//cc:current_cc_toolchain"), + cfg = android_split_transition, + ), + _allowlist_function_transition = attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", ), ), fragments = ["cpp", "android"], diff --git a/mobile/bazel/envoy_mobile_cc_test.bzl b/mobile/bazel/envoy_mobile_cc_test.bzl new file mode 100644 index 0000000000000..7acce77c5134d --- /dev/null +++ b/mobile/bazel/envoy_mobile_cc_test.bzl @@ -0,0 +1,81 @@ +load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_cc_test_library") + +# List of intermediate test libraries that concretely depend on Platform::EngineBuilder. +# Targets in this list will be double-compiled into 2 targets to support both Engine Builders +# (the legacy pristine EngineBuilder and the new MobileEngineBuilder) cleanly. +# Note: Any library depending on these targets is transitively forced to be double-compiled +# as well due to ODR layout constraints. This cascading effect is a minor side-effect given +# that these test libraries reside very close to the leaves/end of the test dependency tree. +LEGACY_BUILDER_LIBRARIES = [ + "base_client_integration_test_lib", + "engine_with_test_server", + "xds_integration_test_lib", +] + +def envoy_cc_test_library_with_engine_builder(name, srcs, deps = [], copts = [], **kwargs): + # 1. Legacy library using pristine EngineBuilder (suffixed) + overridden_deps = [] + for dep in deps: + is_legacy = False + for suffix in LEGACY_BUILDER_LIBRARIES: + if dep.endswith(":" + suffix): + is_legacy = True + break + if is_legacy: + overridden_deps.append(dep + "_legacy_builder") + else: + overridden_deps.append(dep) + + envoy_cc_test_library( + name = name + "_legacy_builder", + srcs = srcs, + copts = copts, + deps = overridden_deps + ["//test/cc:engine_builder_test_shim_legacy_lib"], + **kwargs + ) + + # 2. New library using MobileEngineBuilder under macro switch (default name) + envoy_cc_test_library( + name = name, + srcs = srcs, + copts = copts + ["-DUSE_MOBILE_ENGINE_BUILDER"], + deps = deps + [ + "//test/cc:engine_builder_test_shim_lib", + "//library/cc:mobile_engine_builder_lib", + ], + **kwargs + ) + +def envoy_cc_test_with_engine_builder(name, srcs, deps = [], copts = [], **kwargs): + # 1. Legacy test target (suffixed): rewrite deps to map to _legacy_builder libraries + overridden_deps = [] + for dep in deps: + is_legacy = False + for suffix in LEGACY_BUILDER_LIBRARIES: + if dep.endswith(":" + suffix): + is_legacy = True + break + if is_legacy: + overridden_deps.append(dep + "_legacy_builder") + else: + overridden_deps.append(dep) + + envoy_cc_test( + name = name + "_legacy_builder", + srcs = srcs, + copts = copts, + deps = overridden_deps + ["//test/cc:engine_builder_test_shim_legacy_lib"], + **kwargs + ) + + # 2. Mobile test target (default name): uses default deps mapping directly + envoy_cc_test( + name = name, + srcs = srcs, + copts = copts + ["-DUSE_MOBILE_ENGINE_BUILDER"], + deps = deps + [ + "//test/cc:engine_builder_test_shim_lib", + "//library/cc:mobile_engine_builder_lib", + ], + **kwargs + ) diff --git a/mobile/bazel/envoy_mobile_dependencies.bzl b/mobile/bazel/envoy_mobile_dependencies.bzl index 947443391d113..be9b3d2c3786c 100644 --- a/mobile/bazel/envoy_mobile_dependencies.bzl +++ b/mobile/bazel/envoy_mobile_dependencies.bzl @@ -1,3 +1,4 @@ +load("@bazel_gazelle//:deps.bzl", "go_repository") load("@build_bazel_apple_support//lib:repositories.bzl", "apple_support_dependencies") load("@build_bazel_rules_apple//apple:repositories.bzl", "apple_rules_dependencies") load("@build_bazel_rules_swift//swift:repositories.bzl", "swift_rules_dependencies") @@ -10,6 +11,7 @@ load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies") load("@rules_proto//proto:toolchains.bzl", "rules_proto_toolchains") load("@rules_proto_grpc//:repositories.bzl", "rules_proto_grpc_repos", "rules_proto_grpc_toolchains") load("@rules_python//python:pip.bzl", "pip_parse") +load("@rules_shell//shell:repositories.bzl", "rules_shell_dependencies", "rules_shell_toolchains") def _default_extra_swift_sources_impl(ctx): ctx.file("WORKSPACE", "") @@ -60,16 +62,118 @@ def swift_dependencies(): def kotlin_dependencies(extra_maven_dependencies = []): rules_java_dependencies() + maven_install( + name = "rules_android_maven", + artifacts = [ + "androidx.privacysandbox.tools:tools:1.0.0-alpha06", + "androidx.privacysandbox.tools:tools-apigenerator:1.0.0-alpha06", + "androidx.privacysandbox.tools:tools-apipackager:1.0.0-alpha06", + "androidx.test:core:1.6.0-alpha01", + "androidx.test.ext:junit:1.2.0-alpha01", + "com.android.tools.apkdeployer:apkdeployer:8.11.0-alpha10", + "com.android.tools.build:bundletool:1.18.2", + "com.android.tools:desugar_jdk_libs_minimal:2.1.5", + "com.android.tools:desugar_jdk_libs_configuration_minimal:2.1.5", + "com.android.tools:desugar_jdk_libs_nio:2.1.5", + "com.android.tools:desugar_jdk_libs_configuration_nio:2.1.5", + "com.android.tools:desugar_jdk_libs_configuration:2.1.5", + "com.android.tools:r8:8.9.35", + "org.bouncycastle:bcprov-jdk18on:1.77", + "org.hamcrest:hamcrest-core:2.2", + "org.robolectric:robolectric:4.14.1", + "com.google.flogger:flogger:0.8", + "com.google.flogger:flogger-system-backend:0.8", + "com.google.guava:guava:32.1.2-jre", + "com.google.guava:failureaccess:1.0.1", + "info.picocli:picocli:4.7.4", + "jakarta.inject:jakarta.inject-api:2.0.1", + "junit:junit:4.13.2", + "com.beust:jcommander:1.82", + "com.google.protobuf:protobuf-java:4.35.1", + "com.google.protobuf:protobuf-java-util:4.35.1", + "com.google.code.findbugs:jsr305:3.0.2", + "androidx.databinding:databinding-compiler:8.7.0", + "org.ow2.asm:asm:9.6", + "org.ow2.asm:asm-commons:9.6", + "org.ow2.asm:asm-tree:9.6", + "org.ow2.asm:asm-util:9.6", + "com.android:zipflinger:8.7.0", + "com.android.tools.build:gradle:8.7.0", + "com.android:signflinger:8.7.0", + "com.android.tools.build:aapt2-proto:8.6.1-11315950", + "com.android.tools.build:apksig:8.7.0", + "com.android.tools.build:apkzlib:8.7.0", + "com.google.auto.value:auto-value:1.11.0", + "com.google.auto.value:auto-value-annotations:1.11.0", + "com.google.auto:auto-common:1.2.2", + "com.google.auto.service:auto-service:1.1.1", + "com.google.auto.service:auto-service-annotations:1.1.1", + "com.google.errorprone:error_prone_annotations:2.33.0", + "com.google.errorprone:error_prone_type_annotations:2.33.0", + "com.google.errorprone:error_prone_check_api:2.33.0", + "com.google.errorprone:error_prone_core:2.33.0", + "org.conscrypt:conscrypt-openjdk-uber:2.5.2", + ], + repositories = [ + "https://repo1.maven.org/maven2", + "https://maven.google.com", + ], + use_starlark_android_rules = True, + aar_import_bzl_label = "@rules_android//rules:rules.bzl", + ) + maven_install( + name = "android_ide_common_30_1_3", + aar_import_bzl_label = "@rules_android//rules:rules.bzl", + artifacts = [ + "com.android.tools.layoutlib:layoutlib-api:30.1.3", + "com.android.tools.build:manifest-merger:30.1.3", + "com.android.tools:common:30.1.3", + "com.android.tools:repository:30.1.3", + "com.android.tools.analytics-library:protos:30.1.3", + "com.android.tools.analytics-library:shared:30.1.3", + "com.android.tools.analytics-library:tracker:30.1.3", + "com.android.tools:annotations:30.1.3", + "com.android.tools:sdk-common:30.1.3", + "com.android.tools.build:builder:7.1.3", + "com.android.tools.build:builder-model:7.1.3", + "com.google.protobuf:protobuf-java:4.35.1", + "com.google.protobuf:protobuf-java-util:4.35.1", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], + use_starlark_android_rules = True, + ) + maven_install( + name = "bazel_worker_maven", + artifacts = [ + "com.google.code.gson:gson:2.10.1", + "com.google.errorprone:error_prone_annotations:2.23.0", + "com.google.guava:guava:33.0.0-jre", + "com.google.protobuf:protobuf-java:4.35.1", + "com.google.protobuf:protobuf-java-util:4.35.1", + "junit:junit:4.13.2", + "org.mockito:mockito-core:5.4.0", + "com.google.truth:truth:1.4.0", + ], + aar_import_bzl_label = "@rules_android//rules:rules.bzl", + repositories = [ + "https://repo1.maven.org/maven2", + "https://maven.google.com", + ], + ) maven_install( artifacts = [ "com.google.code.findbugs:jsr305:3.0.2", "androidx.annotation:annotation:1.5.0", # Java Proto Lite - "com.google.protobuf:protobuf-javalite:4.33.1", + "com.google.protobuf:protobuf-javalite:4.35.1", # Kotlin - "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21", - "org.jetbrains.kotlin:kotlin-stdlib-common:1.6.21", - "org.jetbrains.kotlin:kotlin-stdlib:1.6.21", + "org.jetbrains:annotations:23.0.0", + "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.23", + "org.jetbrains.kotlin:kotlin-stdlib-common:1.9.23", + "org.jetbrains.kotlin:kotlin-stdlib:1.9.23", "androidx.recyclerview:recyclerview:1.1.0", "androidx.core:core:1.3.2", # Dokka @@ -107,6 +211,34 @@ def kotlin_dependencies(extra_maven_dependencies = []): rules_proto_dependencies() rules_proto_toolchains() + go_repository( + name = "com_github_google_go_cmp", + importpath = "github.com/google/go-cmp", + sum = "h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=", + version = "v0.5.9", + ) + go_repository( + name = "org_golang_x_sync", + importpath = "golang.org/x/sync", + sum = "h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=", + version = "v0.0.0-20210220032951-036812b2e83c", + ) + go_repository( + name = "com_github_golang_glog", + importpath = "github.com/golang/glog", + version = "v1.1.2", + sum = "h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=", + ) + go_repository( + name = "org_bitbucket_creachadair_stringset", + importpath = "bitbucket.org/creachadair/stringset", + version = "v0.0.14", + sum = "h1:t1ejQyf8utS4GZV/4fM+1gvYucggZkfhb+tMobDxYOE=", + ) + + rules_shell_dependencies() + rules_shell_toolchains() + def python_dependencies(): pip_parse( name = "mobile_pip3", diff --git a/mobile/bazel/envoy_mobile_repositories.bzl b/mobile/bazel/envoy_mobile_repositories.bzl index ec48107d1e0f7..3d8f172b7d857 100644 --- a/mobile/bazel/envoy_mobile_repositories.bzl +++ b/mobile/bazel/envoy_mobile_repositories.bzl @@ -25,9 +25,9 @@ def python_repos(): http_archive( name = "pybind11", build_file = "@pybind11_bazel//:pybind11-BUILD.bazel", - sha256 = "d475978da0cdc2d43b73f30910786759d593a9d8ee05b1b6846d1eb16c6d2e0c", - strip_prefix = "pybind11-2.11.1", - urls = ["https://github.com/pybind/pybind11/archive/refs/tags/v2.11.1.tar.gz"], + sha256 = "e08cb87f4773da97fa7b5f035de8763abc656d87d5773e62f6da0587d1f0ec20", + strip_prefix = "pybind11-2.13.6", + urls = ["https://github.com/pybind/pybind11/archive/refs/tags/v2.13.6.tar.gz"], ) @@ -74,14 +74,6 @@ def swift_repos(): ) def kotlin_repos(): - http_archive( - name = "rules_java", - sha256 = "c0ee60f8757f140c157fc2c7af703d819514de6e025ebf70386d38bdd85fce83", - url = "https://github.com/bazelbuild/rules_java/releases/download/7.12.3/rules_java-7.12.3.tar.gz", - patch_args = ["-p1"], - patches = ["@envoy//bazel:rules_java.patch"], - ) - http_archive( name = "rules_jvm_external", sha256 = "3afe5195069bd379373528899c03a3072f568d33bd96fe037bd43b1f590535e7", @@ -93,6 +85,8 @@ def kotlin_repos(): name = "rules_kotlin", sha256 = "3b772976fec7bdcda1d84b9d39b176589424c047eb2175bed09aac630e50af43", urls = ["https://github.com/bazelbuild/rules_kotlin/releases/download/v1.9.6/rules_kotlin-v1.9.6.tar.gz"], + patch_args = ["-p1"], + patches = ["@envoy_mobile//bazel:rules_kotlin.patch"], ) http_archive( @@ -126,9 +120,9 @@ def kotlin_repos(): def android_repos(): http_archive( name = "rules_android", - urls = ["https://github.com/bazelbuild/rules_android/archive/refs/tags/v0.1.1.zip"], - sha256 = "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", - strip_prefix = "rules_android-0.1.1", + urls = ["https://github.com/bazelbuild/rules_android/releases/download/v0.7.2/rules_android-v0.7.2.tar.gz"], + sha256 = "0da7198c7c8bac7e11e08dca3c434617b8593075858716595672e9aeefbef2a7", + strip_prefix = "rules_android-0.7.2", ) http_archive( name = "rules_android_ndk", diff --git a/mobile/bazel/framework_imports_extractor.bzl b/mobile/bazel/framework_imports_extractor.bzl index 66c8595433ddb..68ffb3dd1c049 100644 --- a/mobile/bazel/framework_imports_extractor.bzl +++ b/mobile/bazel/framework_imports_extractor.bzl @@ -53,6 +53,5 @@ framework_imports_extractor = rule( platform_type = attr.string(default = "ios"), minimum_os_version = attr.string(default = MINIMUM_IOS_VERSION), ), - # fragments = ["apple"], implementation = _framework_imports_extractor, ) diff --git a/mobile/bazel/python.bzl b/mobile/bazel/python.bzl deleted file mode 100644 index f6e4beecc6bbd..0000000000000 --- a/mobile/bazel/python.bzl +++ /dev/null @@ -1,62 +0,0 @@ -abi_bzl_template = """\ -def python_tag(): - return "{python_tag}" - -def abi_tag(): - return "{abi_tag}" -""" - -def _get_python_bin(rctx): - python_label = Label("@python3_12_host//:bin/python3") - python_bin = str(rctx.path(python_label)) - if not python_bin: - fail("failed to get python bin") - return python_bin - -def _get_python_tag(rctx, python_bin): - result = rctx.execute([ - python_bin, - "-c", - "import platform;" + - "assert platform.python_implementation() == 'CPython';" + - "version = platform.python_version_tuple();" + - "print(f'cp{version[0]}{version[1]}')", - ]) - if result.return_code != 0: - fail("Failed to get python tag: " + result.stderr) - return result.stdout.splitlines()[0] - -def _get_abi_tag(rctx, python_bin): - result = rctx.execute([ - python_bin, - "-c", - "import platform;" + - "import sys;" + - "assert platform.python_implementation() == 'CPython';" + - "version = platform.python_version_tuple();" + - "print(f'cp{version[0]}{version[1]}{sys.abiflags}')", - ]) - if result.return_code != 0: - fail("Failed to get abi tag: " + result.stderr) - return result.stdout.splitlines()[0] - -def _declare_python_abi_impl(rctx): - python_bin = _get_python_bin(rctx) - python_tag = _get_python_tag(rctx, python_bin) - abi_tag = _get_abi_tag(rctx, python_bin) - rctx.file("BUILD") - rctx.file( - "abi.bzl", - abi_bzl_template.format( - python_tag = python_tag, - abi_tag = abi_tag, - ), - ) - -declare_python_abi = repository_rule( - implementation = _declare_python_abi_impl, - attrs = { - "python_version": attr.string(mandatory = True), - }, - local = True, -) diff --git a/mobile/bazel/rules_kotlin.patch b/mobile/bazel/rules_kotlin.patch new file mode 100644 index 0000000000000..2ed8e2fb11b34 --- /dev/null +++ b/mobile/bazel/rules_kotlin.patch @@ -0,0 +1,12 @@ +diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel +--- a/third_party/BUILD.bazel ++++ b/third_party/BUILD.bazel +@@ -29,7 +29,7 @@ + + java_import( + name = "android_sdk", +- jars = ["@bazel_tools//tools/android:android_jar"], ++ jars = ["@androidsdk//:platforms/android-30/android.jar"], + neverlink = True, + visibility = ["//visibility:public"], + ) diff --git a/mobile/ci/linux_ci_setup.sh b/mobile/ci/linux_ci_setup.sh index 78cf51f2b0674..17b7107f24af0 100755 --- a/mobile/ci/linux_ci_setup.sh +++ b/mobile/ci/linux_ci_setup.sh @@ -7,6 +7,6 @@ ANDROID_HOME=$ANDROID_SDK_ROOT SDKMANAGER="${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" "${SDKMANAGER}" --install "platform-tools" "platforms;android-30" "${SDKMANAGER}" --uninstall "ndk-bundle" -"${SDKMANAGER}" --install "ndk;26.3.11579264" -"${SDKMANAGER}" --install "build-tools;30.0.2" -echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/26.3.11579264" >> "$GITHUB_ENV" +"${SDKMANAGER}" --install "ndk;29.0.14206865" +"${SDKMANAGER}" --install "build-tools;35.0.0" +echo "ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/29.0.14206865" >> "$GITHUB_ENV" diff --git a/mobile/ci/start_ios_mock_server.py b/mobile/ci/start_ios_mock_server.py index 7e837ecdb688d..e906ffb9ba8b6 100755 --- a/mobile/ci/start_ios_mock_server.py +++ b/mobile/ci/start_ios_mock_server.py @@ -12,7 +12,7 @@ class MockHandler(http.server.SimpleHTTPRequestHandler): - def do_GET(self): + def do_GET(self): # noqa: N802 if self.path == '/ping': self.send_response(200) self.send_header('Content-Type', 'application/json') diff --git a/mobile/docs/root/intro/version_history.rst b/mobile/docs/root/intro/version_history.rst index c9f3660937fe1..1ac9abad25a95 100644 --- a/mobile/docs/root/intro/version_history.rst +++ b/mobile/docs/root/intro/version_history.rst @@ -54,6 +54,7 @@ Features: - build: Add a build feature ``exclude_certificates`` to disable inclusion of the Envoy Mobile certificate list, for use when using platform certificate validation. - build: Add a build feature ``envoy_http_datagrams`` to allow disabling HTTP Datagram support. (:issue:`#23564 <23564>`) - android: log cleared JNI exceptions to platform layer as `jni_cleared_pending_exception` events (:issue:`#26133 <26133>`). +- api: Add support for SCONE (Standardized Communication with Network Elements) via ``enableScone()`` in ``EngineBuilder``. SCONE data is propagated via new fields in ``envoy_stream_intel``. (#44543) 0.5.0 (September 2, 2022) =========================== diff --git a/mobile/library/cc/BUILD b/mobile/library/cc/BUILD index 4e87b6b7fcd07..2ddc08a5ff14c 100644 --- a/mobile/library/cc/BUILD +++ b/mobile/library/cc/BUILD @@ -8,6 +8,23 @@ licenses(["notice"]) # Apache 2 envoy_mobile_package() +envoy_cc_library( + name = "engine_builder_types_lib", + srcs = [ + "engine_builder_types.cc", + ], + hdrs = [ + "engine_builder_types.h", + ], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + "@abseil-cpp//absl/strings", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "network_change_monitor_interface", srcs = [ @@ -18,6 +35,30 @@ envoy_cc_library( deps = [], ) +envoy_cc_library( + name = "engine_builder_base_lib", + hdrs = [ + "engine_builder_base.h", + ], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + ":envoy_engine_cc_lib_no_stamp", + "//library/common:engine_types_lib", + "//library/common:internal_engine_lib_no_stamp", + "@abseil-cpp//absl/status:statusor", + "@envoy//source/common/common:base_logger_lib", + "@envoy//source/common/runtime:runtime_features_lib", + "@envoy//source/server:options_base", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/router/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "engine_builder_lib", srcs = [ @@ -33,8 +74,61 @@ envoy_cc_library( repository = "@envoy", visibility = ["//visibility:public"], deps = [ + ":engine_builder_base_lib", + ":engine_builder_types_lib", + ":envoy_engine_cc_lib_no_stamp", + "@envoy//source/common/common:assert_lib", + "@envoy//source/common/protobuf", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/config/metrics/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/compression/brotli/decompressor/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/compression/gzip/decompressor/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/early_data/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/alternate_protocols_cache/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/decompressor/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/dynamic_forward_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/router/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/http/header_formatters/preserve_case/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/network/dns_resolver/apple/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/network/dns_resolver/getaddrinfo/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/quic/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/raw_buffer/v3:pkg_cc_proto", + "@envoy_mobile//library/common:engine_types_lib", + "@envoy_mobile//library/common/config:certificates_lib", + "@envoy_mobile//library/common/extensions/cert_validator/platform_bridge:platform_bridge_cc_proto", + "@envoy_mobile//library/common/extensions/filters/http/local_error:filter_cc_proto", + "@envoy_mobile//library/common/extensions/filters/http/network_configuration:filter_cc_proto", + "@envoy_mobile//library/common/extensions/filters/http/socket_tag:filter_cc_proto", + "@envoy_mobile//library/common/extensions/quic_packet_writer/platform:platform_packet_writer_proto_cc_proto", + "@envoy_mobile//library/common/types:matcher_data_lib", + ] + select({ + "@envoy//bazel:apple": [ + "@envoy_mobile//library/common/network:apple_proxy_resolution_lib", + ], + "//conditions:default": [], + }), +) + +envoy_cc_library( + name = "mobile_engine_builder_lib", + srcs = [ + "mobile_engine_builder.cc", + ], + hdrs = [ + "mobile_engine_builder.h", + ], + copts = select({ + "//bazel:exclude_certificates": ["-DEXCLUDE_CERTIFICATES"], + "//conditions:default": [], + }), + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + ":engine_builder_base_lib", + ":engine_builder_types_lib", ":envoy_engine_cc_lib_no_stamp", - "@abseil-cpp//absl/types:optional", "@envoy//source/common/common:assert_lib", "@envoy//source/common/protobuf", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", @@ -42,6 +136,7 @@ envoy_cc_library( "@envoy_api//envoy/config/metrics/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/compression/brotli/decompressor/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/compression/gzip/decompressor/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/early_data/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/alternate_protocols_cache/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/decompressor/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/http/dynamic_forward_proxy/v3:pkg_cc_proto", @@ -96,7 +191,6 @@ envoy_cc_library( "//library/common/api:c_types", "//library/common/bridge:utility_lib", "//library/common/extensions/key_value/platform:config", - "@abseil-cpp//absl/types:optional", "@envoy//source/common/buffer:buffer_lib", "@envoy//source/common/http:header_map_lib", "@envoy//source/common/http:utility_lib", @@ -110,6 +204,7 @@ envoy_cc_library( deps = [ ":engine_builder_lib", ":envoy_engine_cc_lib_no_stamp", + ":mobile_engine_builder_lib", "//library/common:engine_common_lib_stamped", ], ) diff --git a/mobile/library/cc/engine.cc b/mobile/library/cc/engine.cc index 68c2895604d4d..89a3591d3945f 100644 --- a/mobile/library/cc/engine.cc +++ b/mobile/library/cc/engine.cc @@ -1,10 +1,14 @@ #include "engine.h" +#include + #include "absl/status/status.h" #include "absl/strings/string_view.h" #include "library/common/internal_engine.h" +#include "library/common/network/socket_tag_socket_option_impl.h" #include "library/common/system/system_helper.h" #include "library/common/types/c_types.h" +#include "source/common/common/assert.h" namespace Envoy { namespace Platform { @@ -31,8 +35,8 @@ Engine::~Engine() { // because they either require or will require a weak ptr // which can't be provided from inside of the constructor // because of how std::enable_shared_from_this works -StreamClientSharedPtr Engine::streamClient() { - return std::make_shared(shared_from_this()); +StreamClientSharedPtr Engine::streamClient(absl::string_view listener_name) { + return std::make_shared(shared_from_this(), listener_name); } std::string Engine::dumpStats() { return engine_->dumpStats(); } @@ -62,5 +66,9 @@ envoy_status_t Engine::setProxySettings(absl::string_view host, const uint16_t p return engine_->setProxySettings(host, port); } +void Engine::drainConnectionsBySocketTag(uint32_t tag) { + engine_->drainConnectionsBySocketTag(tag); +} + } // namespace Platform } // namespace Envoy diff --git a/mobile/library/cc/engine.h b/mobile/library/cc/engine.h index 7fe0f23fbb35c..d225dd05aac66 100644 --- a/mobile/library/cc/engine.h +++ b/mobile/library/cc/engine.h @@ -29,7 +29,12 @@ class Engine : public std::enable_shared_from_this, public NetworkChange ~Engine(); std::string dumpStats(); - StreamClientSharedPtr streamClient(); + /** + * Creates a new StreamClient. + * @param listener_name The name of the API listener to route streams to. + * If empty, the default listener will be used. + */ + StreamClientSharedPtr streamClient(absl::string_view listener_name = ""); int64_t getInternalEngineHandle() const; void onDefaultNetworkChangeEvent(int network); // TODO(abeyad): Remove once migrated to onDefaultNetworkChangeEvent(). @@ -40,6 +45,7 @@ class Engine : public std::enable_shared_from_this, public NetworkChange envoy_status_t setProxySettings(absl::string_view host, const uint16_t port); envoy_status_t terminate(); + void drainConnectionsBySocketTag(uint32_t tag); Envoy::InternalEngine* engine() { return engine_; } private: @@ -47,6 +53,7 @@ class Engine : public std::enable_shared_from_this, public NetworkChange // required to access private constructor friend class EngineBuilder; + template friend class EngineBuilderBase; // required to use envoy_engine_t without exposing it publicly friend class StreamPrototype; // for testing only diff --git a/mobile/library/cc/engine_builder.cc b/mobile/library/cc/engine_builder.cc index fb104d31a477e..b9e040ba03158 100644 --- a/mobile/library/cc/engine_builder.cc +++ b/mobile/library/cc/engine_builder.cc @@ -3,6 +3,7 @@ #include #include +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "envoy/config/core/v3/socket_option.pb.h" #include "envoy/config/metrics/v3/metrics_service.pb.h" #include "envoy/extensions/compression/brotli/decompressor/v3/brotli.pb.h" @@ -12,6 +13,7 @@ #include "envoy/extensions/filters/http/dynamic_forward_proxy/v3/dynamic_forward_proxy.pb.h" #include "envoy/extensions/filters/http/router/v3/router.pb.h" #include "envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.pb.h" +#include "envoy/extensions/early_data/v3/default_early_data_policy.pb.h" #if defined(__APPLE__) #include "envoy/extensions/network/dns_resolver/apple/v3/apple_dns_resolver.pb.h" @@ -211,6 +213,11 @@ EngineBuilder& EngineBuilder::setPerTryIdleTimeoutSeconds(int per_try_idle_timeo return *this; } +EngineBuilder& EngineBuilder::setRequestTimeoutMilliseconds(int request_timeout_ms) { + request_timeout_ms_ = request_timeout_ms; + return *this; +} + EngineBuilder& EngineBuilder::enableGzipDecompression(bool gzip_decompression_on) { gzip_decompression_filter_ = gzip_decompression_on; return *this; @@ -231,6 +238,16 @@ EngineBuilder& EngineBuilder::enableHttp3(bool http3_on) { return *this; } +EngineBuilder& EngineBuilder::enableEarlyData(bool early_data_on) { + enable_early_data_ = early_data_on; + return *this; +} + +EngineBuilder& EngineBuilder::enableScone(bool enable) { + scone_enabled_ = enable; + return *this; +} + EngineBuilder& EngineBuilder::addQuicConnectionOption(std::string option) { quic_connection_options_.push_back(std::move(option)); return *this; @@ -315,6 +332,10 @@ EngineBuilder& EngineBuilder::setMaxConcurrentStreams(int max_concurrent_streams EngineBuilder& EngineBuilder::enablePlatformCertificatesValidation(bool platform_certificates_validation_on) { + if (use_worker_thread_) { + // Platform certificate validation is not supported with worker thread. + return *this; + } platform_certificates_validation_on_ = platform_certificates_validation_on; return *this; } @@ -359,16 +380,29 @@ std::string EngineBuilder::nativeNameToConfig(absl::string_view name) { envoymobile::extensions::filters::http::platform_bridge::PlatformBridge proto_config; proto_config.set_platform_filter_name(name); std::string ret; - proto_config.SerializeToString(&ret); + std::ignore = proto_config.SerializeToString(&ret); Protobuf::Any any_config; any_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.platform_bridge.PlatformBridge"); any_config.set_value(ret); - any_config.SerializeToString(&ret); + std::ignore = any_config.SerializeToString(&ret); return ret; #endif } +EngineBuilder& EngineBuilder::enableWorkerThread(bool use_worker_thread) { + use_worker_thread_ = use_worker_thread; + if (use_worker_thread_) { + // Platform certificate validation and system proxy settings are not supported with worker + // thread. + platform_certificates_validation_on_ = false; +#ifdef __APPLE__ + respect_system_proxy_settings_ = false; +#endif + } + return *this; +} + EngineBuilder& EngineBuilder::addPlatformFilter(const std::string& name) { addNativeFilter("envoy.filters.http.platform_bridge", nativeNameToConfig(name)); return *this; @@ -434,6 +468,10 @@ EngineBuilder::setMaxTimeOnNonDefaultNetworkSeconds(int max_time_on_non_default_ #if defined(__APPLE__) EngineBuilder& EngineBuilder::respectSystemProxySettings(bool value, int refresh_interval_secs) { + if (use_worker_thread_) { + // System proxy settings are not supported with worker thread. + return *this; + } respect_system_proxy_settings_ = value; if (refresh_interval_secs > 0) { proxy_settings_refresh_interval_secs_ = refresh_interval_secs; @@ -448,91 +486,6 @@ EngineBuilder& EngineBuilder::setIosNetworkServiceType(int ios_network_service_t #endif #ifdef ENVOY_MOBILE_XDS -XdsBuilder::XdsBuilder(std::string xds_server_address, const uint32_t xds_server_port) - : xds_server_address_(std::move(xds_server_address)), xds_server_port_(xds_server_port) {} - -XdsBuilder& XdsBuilder::addInitialStreamHeader(std::string header, std::string value) { - envoy::config::core::v3::HeaderValue header_value; - header_value.set_key(std::move(header)); - header_value.set_value(std::move(value)); - xds_initial_grpc_metadata_.emplace_back(std::move(header_value)); - return *this; -} - -XdsBuilder& XdsBuilder::setSslRootCerts(std::string root_certs) { - ssl_root_certs_ = std::move(root_certs); - return *this; -} - -XdsBuilder& XdsBuilder::addRuntimeDiscoveryService(std::string resource_name, - const int timeout_in_seconds) { - rtds_resource_name_ = std::move(resource_name); - rtds_timeout_in_seconds_ = timeout_in_seconds > 0 ? timeout_in_seconds : DefaultXdsTimeout; - return *this; -} - -XdsBuilder& XdsBuilder::addClusterDiscoveryService(std::string cds_resources_locator, - const int timeout_in_seconds) { - enable_cds_ = true; - cds_resources_locator_ = std::move(cds_resources_locator); - cds_timeout_in_seconds_ = timeout_in_seconds > 0 ? timeout_in_seconds : DefaultXdsTimeout; - return *this; -} - -void XdsBuilder::build(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const { - auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); - ads_config->set_transport_api_version(envoy::config::core::v3::ApiVersion::V3); - ads_config->set_set_node_on_first_message_only(true); - ads_config->set_api_type(envoy::config::core::v3::ApiConfigSource::GRPC); - - auto& grpc_service = *ads_config->add_grpc_services(); - grpc_service.mutable_envoy_grpc()->set_cluster_name("base"); - grpc_service.mutable_envoy_grpc()->set_authority( - absl::StrCat(xds_server_address_, ":", xds_server_port_)); - - if (!xds_initial_grpc_metadata_.empty()) { - grpc_service.mutable_initial_metadata()->Assign(xds_initial_grpc_metadata_.begin(), - xds_initial_grpc_metadata_.end()); - } - - if (!rtds_resource_name_.empty()) { - auto* layered_runtime = bootstrap.mutable_layered_runtime(); - auto* layer = layered_runtime->add_layers(); - layer->set_name("rtds_layer"); - auto* rtds_layer = layer->mutable_rtds_layer(); - rtds_layer->set_name(rtds_resource_name_); - auto* rtds_config = rtds_layer->mutable_rtds_config(); - rtds_config->mutable_ads(); - rtds_config->set_resource_api_version(envoy::config::core::v3::ApiVersion::V3); - rtds_config->mutable_initial_fetch_timeout()->set_seconds(rtds_timeout_in_seconds_); - } - - if (enable_cds_) { - auto* cds_config = bootstrap.mutable_dynamic_resources()->mutable_cds_config(); - if (cds_resources_locator_.empty()) { - cds_config->mutable_ads(); - } else { - bootstrap.mutable_dynamic_resources()->set_cds_resources_locator(cds_resources_locator_); - cds_config->mutable_api_config_source()->set_api_type( - envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC); - cds_config->mutable_api_config_source()->set_transport_api_version( - envoy::config::core::v3::ApiVersion::V3); - } - cds_config->mutable_initial_fetch_timeout()->set_seconds(cds_timeout_in_seconds_); - cds_config->set_resource_api_version(envoy::config::core::v3::ApiVersion::V3); - bootstrap.add_node_context_params("cluster"); - // Stat prefixes that we use in tests. - auto* list = - bootstrap.mutable_stats_config()->mutable_stats_matcher()->mutable_inclusion_list(); - list->add_patterns()->set_exact("cluster_manager.active_clusters"); - list->add_patterns()->set_exact("cluster_manager.cluster_added"); - list->add_patterns()->set_exact("cluster_manager.cluster_updated"); - list->add_patterns()->set_exact("cluster_manager.cluster_removed"); - // Allow SDS related stats. - list->add_patterns()->mutable_safe_regex()->set_regex("sds\\..*"); - list->add_patterns()->mutable_safe_regex()->set_regex(".*\\.ssl_context_update_by_sds"); - } -} EngineBuilder& EngineBuilder::setXds(XdsBuilder xds_builder) { xds_builder_ = std::move(xds_builder); @@ -572,21 +525,33 @@ std::unique_ptr EngineBuilder::generate route->mutable_per_request_buffer_limit_bytes()->set_value(4096); auto* route_to = route->mutable_route(); route_to->set_cluster_header("x-envoy-mobile-cluster"); - route_to->mutable_timeout()->set_seconds(0); + if (request_timeout_ms_ > 0) { + route_to->mutable_timeout()->set_nanos((request_timeout_ms_ % 1000) * 1000000); + route_to->mutable_timeout()->set_seconds(request_timeout_ms_ / 1000); + } else { + route_to->mutable_timeout()->set_seconds(0); + } route_to->mutable_retry_policy()->mutable_per_try_idle_timeout()->set_seconds( per_try_idle_timeout_seconds_); auto* backoff = route_to->mutable_retry_policy()->mutable_retry_back_off(); backoff->mutable_base_interval()->set_nanos(250000000); backoff->mutable_max_interval()->set_seconds(60); + if (!enable_early_data_) { + auto* early_data = route_to->mutable_early_data_policy(); + early_data->set_name("envoy.route.early_data_policy.default"); + ::envoy::extensions::early_data::v3::DefaultEarlyDataPolicy config; + std::ignore = early_data->mutable_typed_config()->PackFrom(config); + } + for (auto filter = native_filter_chain_.rbegin(); filter != native_filter_chain_.rend(); ++filter) { auto* native_filter = hcm->add_http_filters(); native_filter->set_name(filter->name_); if (!filter->textproto_typed_config_.empty()) { #ifdef ENVOY_ENABLE_FULL_PROTOS - Protobuf::TextFormat::ParseFromString((*filter).textproto_typed_config_, - native_filter->mutable_typed_config()); + std::ignore = Protobuf::TextFormat::ParseFromString((*filter).textproto_typed_config_, + native_filter->mutable_typed_config()); RELEASE_ASSERT(!native_filter->typed_config().DebugString().empty(), "Failed to parse: " + (*filter).textproto_typed_config_); #else @@ -604,7 +569,7 @@ std::unique_ptr EngineBuilder::generate envoy::extensions::filters::http::alternate_protocols_cache::v3::FilterConfig cache_config; auto* cache_filter = hcm->add_http_filters(); cache_filter->set_name("alternate_protocols_cache"); - cache_filter->mutable_typed_config()->PackFrom(cache_config); + std::ignore = cache_filter->mutable_typed_config()->PackFrom(cache_config); } if (gzip_decompression_filter_) { @@ -612,8 +577,9 @@ std::unique_ptr EngineBuilder::generate gzip_config.mutable_window_bits()->set_value(15); envoy::extensions::filters::http::decompressor::v3::Decompressor decompressor_config; decompressor_config.mutable_decompressor_library()->set_name("gzip"); - decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( - gzip_config); + std::ignore = + decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( + gzip_config); auto* common_request = decompressor_config.mutable_request_direction_config()->mutable_common_config(); common_request->mutable_enabled()->mutable_default_value(); @@ -623,14 +589,15 @@ std::unique_ptr EngineBuilder::generate ->set_ignore_no_transform_header(true); auto* gzip_filter = hcm->add_http_filters(); gzip_filter->set_name("envoy.filters.http.decompressor"); - gzip_filter->mutable_typed_config()->PackFrom(decompressor_config); + std::ignore = gzip_filter->mutable_typed_config()->PackFrom(decompressor_config); } if (brotli_decompression_filter_) { envoy::extensions::compression::brotli::decompressor::v3::Brotli brotli_config; envoy::extensions::filters::http::decompressor::v3::Decompressor decompressor_config; decompressor_config.mutable_decompressor_library()->set_name("text_optimized"); - decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( - brotli_config); + std::ignore = + decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( + brotli_config); auto* common_request = decompressor_config.mutable_request_direction_config()->mutable_common_config(); common_request->mutable_enabled()->mutable_default_value(); @@ -640,13 +607,13 @@ std::unique_ptr EngineBuilder::generate ->set_ignore_no_transform_header(true); auto* brotli_filter = hcm->add_http_filters(); brotli_filter->set_name("envoy.filters.http.decompressor"); - brotli_filter->mutable_typed_config()->PackFrom(decompressor_config); + std::ignore = brotli_filter->mutable_typed_config()->PackFrom(decompressor_config); } if (socket_tagging_filter_) { envoymobile::extensions::filters::http::socket_tag::SocketTag tag_config; auto* tag_filter = hcm->add_http_filters(); tag_filter->set_name("envoy.filters.http.socket_tag"); - tag_filter->mutable_typed_config()->PackFrom(tag_config); + std::ignore = tag_filter->mutable_typed_config()->PackFrom(tag_config); } // Set up the always-present filters @@ -656,12 +623,12 @@ std::unique_ptr EngineBuilder::generate network_config.set_enable_interface_binding(enable_interface_binding_); auto* network_filter = hcm->add_http_filters(); network_filter->set_name("envoy.filters.http.network_configuration"); - network_filter->mutable_typed_config()->PackFrom(network_config); + std::ignore = network_filter->mutable_typed_config()->PackFrom(network_config); envoymobile::extensions::filters::http::local_error::LocalError local_config; auto* local_filter = hcm->add_http_filters(); local_filter->set_name("envoy.filters.http.local_error"); - local_filter->mutable_typed_config()->PackFrom(local_config); + std::ignore = local_filter->mutable_typed_config()->PackFrom(local_config); envoy::extensions::filters::http::dynamic_forward_proxy::v3::FilterConfig dfp_config; auto* dns_cache_config = dfp_config.mutable_dns_cache_config(); @@ -683,10 +650,10 @@ std::unique_ptr EngineBuilder::generate kv_config.set_max_entries(100); dns_cache_config->mutable_key_value_config()->mutable_config()->set_name( "envoy.key_value.platform"); - dns_cache_config->mutable_key_value_config() - ->mutable_config() - ->mutable_typed_config() - ->PackFrom(kv_config); + std::ignore = dns_cache_config->mutable_key_value_config() + ->mutable_config() + ->mutable_typed_config() + ->PackFrom(kv_config); } if (dns_resolver_config_.has_value()) { @@ -696,8 +663,9 @@ std::unique_ptr EngineBuilder::generate envoy::extensions::network::dns_resolver::apple::v3::AppleDnsResolverConfig resolver_config; dns_cache_config->mutable_typed_dns_resolver_config()->set_name( "envoy.network.dns_resolver.apple"); - dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( - resolver_config); + std::ignore = + dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( + resolver_config); #else envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig resolver_config; @@ -707,8 +675,9 @@ std::unique_ptr EngineBuilder::generate resolver_config.mutable_num_resolver_threads()->set_value(getaddrinfo_num_threads_); dns_cache_config->mutable_typed_dns_resolver_config()->set_name( "envoy.network.dns_resolver.getaddrinfo"); - dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( - resolver_config); + std::ignore = + dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( + resolver_config); #endif } @@ -720,12 +689,12 @@ std::unique_ptr EngineBuilder::generate auto* dfp_filter = hcm->add_http_filters(); dfp_filter->set_name("envoy.filters.http.dynamic_forward_proxy"); - dfp_filter->mutable_typed_config()->PackFrom(dfp_config); + std::ignore = dfp_filter->mutable_typed_config()->PackFrom(dfp_config); auto* router_filter = hcm->add_http_filters(); envoy::extensions::filters::http::router::v3::Router router_config; router_filter->set_name("envoy.router"); - router_filter->mutable_typed_config()->PackFrom(router_config); + std::ignore = router_filter->mutable_typed_config()->PackFrom(router_config); auto* static_resources = bootstrap->mutable_static_resources(); @@ -737,7 +706,8 @@ std::unique_ptr EngineBuilder::generate base_address->mutable_socket_address()->set_address("0.0.0.0"); base_address->mutable_socket_address()->set_port_value(10000); base_listener->mutable_per_connection_buffer_limit_bytes()->set_value(10485760); - base_listener->mutable_api_listener()->mutable_api_listener()->PackFrom(api_listener_config); + std::ignore = + base_listener->mutable_api_listener()->mutable_api_listener()->PackFrom(api_listener_config); // Basic TLS config. envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext tls_socket; @@ -763,7 +733,8 @@ std::unique_ptr EngineBuilder::generate } validation->mutable_custom_validator_config()->set_name( "envoy_mobile.cert_validator.platform_bridge_cert_validator"); - validation->mutable_custom_validator_config()->mutable_typed_config()->PackFrom(validator); + std::ignore = + validation->mutable_custom_validator_config()->mutable_typed_config()->PackFrom(validator); } else { std::string certs; #ifdef ENVOY_MOBILE_XDS @@ -791,11 +762,12 @@ std::unique_ptr EngineBuilder::generate envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport ssl_proxy_socket; ssl_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.tls"); - ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); + std::ignore = + ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); envoy::config::core::v3::TransportSocket base_tls_socket; base_tls_socket.set_name("envoy.transport_sockets.http_11_proxy"); - base_tls_socket.mutable_typed_config()->PackFrom(ssl_proxy_socket); + std::ignore = base_tls_socket.mutable_typed_config()->PackFrom(ssl_proxy_socket); envoy::extensions::upstreams::http::v3::HttpProtocolOptions h2_protocol_options; h2_protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); @@ -806,7 +778,7 @@ std::unique_ptr EngineBuilder::generate envoy::config::cluster::v3::Cluster::CustomClusterType base_cluster_type; base_cluster_config.mutable_dns_cache_config()->CopyFrom(*dns_cache_config); base_cluster_type.set_name("envoy.clusters.dynamic_forward_proxy"); - base_cluster_type.mutable_typed_config()->PackFrom(base_cluster_config); + std::ignore = base_cluster_type.mutable_typed_config()->PackFrom(base_cluster_config); auto* upstream_opts = base_cluster->mutable_upstream_connection_options(); upstream_opts->set_set_local_interface_name_on_upstream_connections(true); @@ -853,24 +825,26 @@ std::unique_ptr EngineBuilder::generate auto* h1_options = alpn_options.mutable_auto_config()->mutable_http_protocol_options(); auto* formatter = h1_options->mutable_header_key_format()->mutable_stateful_formatter(); formatter->set_name("preserve_case"); - formatter->mutable_typed_config()->PackFrom(preserve_case_config); + std::ignore = formatter->mutable_typed_config()->PackFrom(preserve_case_config); // Base cluster base_cluster->set_name("base"); base_cluster->mutable_connect_timeout()->set_seconds(connect_timeout_seconds_); base_cluster->set_lb_policy(envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED); - (*base_cluster->mutable_typed_extension_protocol_options()) - ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] - .PackFrom(alpn_options); + std::ignore = (*base_cluster->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(alpn_options); base_cluster->mutable_cluster_type()->CopyFrom(base_cluster_type); base_cluster->mutable_transport_socket()->set_name("envoy.transport_sockets.http_11_proxy"); - base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(ssl_proxy_socket); + std::ignore = + base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(ssl_proxy_socket); // Base clear-text cluster set up envoy::extensions::transport_sockets::raw_buffer::v3::RawBuffer raw_buffer; envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport cleartext_proxy_socket; - cleartext_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(raw_buffer); + std::ignore = cleartext_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom( + raw_buffer); cleartext_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.raw_buffer"); envoy::extensions::upstreams::http::v3::HttpProtocolOptions h1_protocol_options; h1_protocol_options.mutable_upstream_http_protocol_options()->set_auto_sni(true); @@ -885,17 +859,19 @@ std::unique_ptr EngineBuilder::generate base_clear->set_lb_policy(envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED); base_clear->mutable_cluster_type()->CopyFrom(base_cluster_type); base_clear->mutable_transport_socket()->set_name("envoy.transport_sockets.http_11_proxy"); - base_clear->mutable_transport_socket()->mutable_typed_config()->PackFrom(cleartext_proxy_socket); + std::ignore = base_clear->mutable_transport_socket()->mutable_typed_config()->PackFrom( + cleartext_proxy_socket); base_clear->mutable_upstream_connection_options()->CopyFrom( *base_cluster->mutable_upstream_connection_options()); base_clear->mutable_circuit_breakers()->CopyFrom(*base_cluster->mutable_circuit_breakers()); - (*base_clear->mutable_typed_extension_protocol_options()) - ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] - .PackFrom(h1_protocol_options); + std::ignore = (*base_clear->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(h1_protocol_options); // Edit and re-pack tls_socket.mutable_common_tls_context()->add_alpn_protocols("h2"); - ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); + std::ignore = + ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); // Edit base cluster to be an HTTP/3 cluster. if (enable_http3_) { @@ -904,7 +880,8 @@ std::unique_ptr EngineBuilder::generate h3_inner_socket.mutable_upstream_tls_context()->CopyFrom(tls_socket); envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport h3_proxy_socket; - h3_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(h3_inner_socket); + std::ignore = h3_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom( + h3_inner_socket); h3_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.quic"); auto* quic_protocol_options = alpn_options.mutable_auto_config() @@ -953,11 +930,16 @@ std::unique_ptr EngineBuilder::generate } } + if (scone_enabled_) { + quic_protocol_options->mutable_enable_scone()->set_value(true); + } + if (use_quic_platform_packet_writer_ || enable_quic_connection_migration_) { envoy_mobile::extensions::quic_packet_writer::platform::QuicPlatformPacketWriterConfig writer_config; - quic_protocol_options->mutable_client_packet_writer()->mutable_typed_config()->PackFrom( - writer_config); + std::ignore = + quic_protocol_options->mutable_client_packet_writer()->mutable_typed_config()->PackFrom( + writer_config); quic_protocol_options->mutable_client_packet_writer()->set_name( "envoy.quic.packet_writer.platform"); } @@ -977,10 +959,11 @@ std::unique_ptr EngineBuilder::generate ->add_canonical_suffixes(suffix); } - base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(h3_proxy_socket); - (*base_cluster->mutable_typed_extension_protocol_options()) - ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] - .PackFrom(alpn_options); + std::ignore = + base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(h3_proxy_socket); + std::ignore = (*base_cluster->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(alpn_options); // Set the upstream connections UDP socket receive buffer size. The operating system defaults // are usually too small for QUIC. @@ -1119,19 +1102,25 @@ std::unique_ptr EngineBuilder::generate } #endif // ENVOY_MOBILE_XDS - envoy::config::listener::v3::ApiListenerManager api; + envoy::config::bootstrap::v3::ApiListenerManager api; + if (!use_worker_thread_) { + api.set_threading_model(envoy::config::bootstrap::v3::ApiListenerManager::MAIN_THREAD_ONLY); + } else { + api.set_threading_model( + envoy::config::bootstrap::v3::ApiListenerManager::STANDALONE_WORKER_THREAD); + } auto* listener_manager = bootstrap->mutable_listener_manager(); - listener_manager->mutable_typed_config()->PackFrom(api); + std::ignore = listener_manager->mutable_typed_config()->PackFrom(api); listener_manager->set_name("envoy.listener_manager_impl.api"); return bootstrap; } EngineSharedPtr EngineBuilder::build() { - InternalEngine* envoy_engine = absl::IgnoreLeak( - new InternalEngine(std::move(callbacks_), std::move(logger_), std::move(event_tracker_), - network_thread_priority_, high_watermark_, - disable_dns_refresh_on_network_change_, enable_logger_)); + InternalEngine* envoy_engine = absl::IgnoreLeak(new InternalEngine( + std::move(callbacks_), std::move(logger_), std::move(event_tracker_), + network_thread_priority_, high_watermark_, enable_logger_, use_worker_thread_)); + envoy_engine->disableDnsRefreshOnNetworkChange(disable_dns_refresh_on_network_change_); for (const auto& [name, store] : key_value_stores_) { // TODO(goaway): This leaks, but it's tied to the life of the engine. diff --git a/mobile/library/cc/engine_builder.h b/mobile/library/cc/engine_builder.h index e0a8e535fcd99..7ae90c432dd88 100644 --- a/mobile/library/cc/engine_builder.h +++ b/mobile/library/cc/engine_builder.h @@ -12,109 +12,17 @@ #include "source/common/protobuf/protobuf.h" #include "absl/container/flat_hash_map.h" -#include "absl/types/optional.h" +#include #include "library/cc/engine.h" #include "library/cc/key_value_store.h" #include "library/cc/string_accessor.h" #include "library/common/engine_types.h" +#include "library/cc/engine_builder_types.h" + namespace Envoy { namespace Platform { -// Represents the locality information in the Bootstrap's node, as defined in: -// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto#envoy-v3-api-msg-config-core-v3-locality -struct NodeLocality { - std::string region; - std::string zone; - std::string sub_zone; -}; - -#ifdef ENVOY_MOBILE_XDS -constexpr int DefaultXdsTimeout = 5; - -// Forward declaration so it can be referenced by XdsBuilder. -class EngineBuilder; - -// A class for building the xDS configuration for the Envoy Mobile engine. -// xDS is a protocol for dynamic configuration of Envoy instances, more information can be found in: -// https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol. -// -// This class is typically used as input to the EngineBuilder's setXds() method. -class XdsBuilder final { -public: - // `xds_server_address`: the host name or IP address of the xDS management server. The xDS server - // must support the ADS protocol - // (https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration#aggregated-xds-ads). - // `xds_server_port`: the port on which the xDS management server listens for ADS discovery - // requests. - XdsBuilder(std::string xds_server_address, const uint32_t xds_server_port); - - // Adds a header to the initial HTTP metadata headers sent on the gRPC stream. - // - // A common use for the initial metadata headers is for authentication to the xDS management - // server. - // - // For example, if using API keys to authenticate to Traffic Director on GCP (see - // https://cloud.google.com/docs/authentication/api-keys for details), invoke: - // builder.addInitialStreamHeader("x-goog-api-key", api_key_token) - // .addInitialStreamHeader("X-Android-Package", app_package_name) - // .addInitialStreamHeader("X-Android-Cert", sha1_key_fingerprint); - XdsBuilder& addInitialStreamHeader(std::string header, std::string value); - - // Sets the PEM-encoded server root certificates used to negotiate the TLS handshake for the gRPC - // connection. If no root certs are specified, the operating system defaults are used. - XdsBuilder& setSslRootCerts(std::string root_certs); - - // Adds Runtime Discovery Service (RTDS) to the Runtime layers of the Bootstrap configuration, - // to retrieve dynamic runtime configuration via the xDS management server. - // - // `resource_name`: The runtime config resource to subscribe to. - // `timeout_in_seconds`: specifies the `initial_fetch_timeout` field on the - // api.v3.core.ConfigSource. Unlike the ConfigSource default of 15s, we set a default fetch - // timeout value of 5s, to prevent mobile app initialization from stalling. The default - // parameter value may change through the course of experimentation and no assumptions should - // be made of its exact value. - XdsBuilder& addRuntimeDiscoveryService(std::string resource_name, - int timeout_in_seconds = DefaultXdsTimeout); - - // Adds the Cluster Discovery Service (CDS) configuration for retrieving dynamic cluster resources - // via the xDS management server. - // - // `cds_resources_locator`: the xdstp:// URI for subscribing to the cluster resources. - // If not using xdstp, then `cds_resources_locator` should be set to the empty string. - // `timeout_in_seconds`: specifies the `initial_fetch_timeout` field on the - // api.v3.core.ConfigSource. Unlike the ConfigSource default of 15s, we set a default fetch - // timeout value of 5s, to prevent mobile app initialization from stalling. The default - // parameter value may change through the course of experimentation and no assumptions should - // be made of its exact value. - XdsBuilder& addClusterDiscoveryService(std::string cds_resources_locator = "", - int timeout_in_seconds = DefaultXdsTimeout); - -protected: - // Sets the xDS configuration specified on this XdsBuilder instance on the Bootstrap proto - // provided as an input parameter. - // - // This method takes in a modifiable Bootstrap proto pointer because returning a new Bootstrap - // proto would rely on proto's MergeFrom behavior, which can lead to unexpected results in the - // Bootstrap config. - void build(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const; - -private: - // Required so that EngineBuilder can call the XdsBuilder's protected build() method. - friend class EngineBuilder; - - std::string xds_server_address_; - uint32_t xds_server_port_; - std::vector xds_initial_grpc_metadata_; - std::string ssl_root_certs_; - std::string rtds_resource_name_; - int rtds_timeout_in_seconds_ = DefaultXdsTimeout; - bool enable_cds_ = false; - std::string cds_resources_locator_; - int cds_timeout_in_seconds_ = DefaultXdsTimeout; -}; -#endif // ENVOY_MOBILE_XDS - // The C++ Engine builder creates a structured bootstrap proto and modifies it through parameters // set through the EngineBuilder API calls to produce the Bootstrap config that the Engine is // created from. @@ -153,10 +61,18 @@ class EngineBuilder { EngineBuilder& setDeviceOs(std::string app_id); EngineBuilder& setStreamIdleTimeoutSeconds(int stream_idle_timeout_seconds); EngineBuilder& setPerTryIdleTimeoutSeconds(int per_try_idle_timeout_seconds); + EngineBuilder& setRequestTimeoutMilliseconds(int request_timeout_ms); EngineBuilder& enableGzipDecompression(bool gzip_decompression_on); EngineBuilder& enableBrotliDecompression(bool brotli_decompression_on); EngineBuilder& enableSocketTagging(bool socket_tagging_on); EngineBuilder& enableHttp3(bool http3_on); + // If true, all HTTP requests are handled on a dedicated worker thread instead of on the Envoy + // main thread which also handles all xDS requests. + // Note: Engine in worker thread model doesn't support platform certificate validation and system + // proxy settings. And these settings will be ignored if worker thread model is enabled. + EngineBuilder& enableWorkerThread(bool use_worker_thread); + EngineBuilder& enableEarlyData(bool early_data_on); + EngineBuilder& enableScone(bool enable); EngineBuilder& addQuicConnectionOption(std::string option); EngineBuilder& addQuicClientConnectionOption(std::string option); // Deprecated, use addQuicConnectionOption() instead. @@ -296,7 +212,7 @@ class EngineBuilder { int dns_query_timeout_seconds_ = 120; bool disable_dns_refresh_on_failure_{false}; bool disable_dns_refresh_on_network_change_{false}; - absl::optional dns_num_retries_ = 3; + std::optional dns_num_retries_ = 3; uint32_t getaddrinfo_num_threads_ = 1; int h2_connection_keepalive_idle_interval_milliseconds_ = 100000000; int h2_connection_keepalive_timeout_seconds_ = 15; @@ -305,14 +221,15 @@ class EngineBuilder { std::string device_os_ = "unspecified"; int stream_idle_timeout_seconds_ = 15; int per_try_idle_timeout_seconds_ = 15; + int request_timeout_ms_ = 0; bool gzip_decompression_filter_ = true; bool brotli_decompression_filter_ = false; bool socket_tagging_filter_ = false; bool platform_certificates_validation_on_ = false; bool dns_cache_on_ = false; int dns_cache_save_interval_seconds_ = 1; - absl::optional network_thread_priority_ = absl::nullopt; - absl::optional high_watermark_ = absl::nullopt; + std::optional network_thread_priority_ = std::nullopt; + std::optional high_watermark_ = std::nullopt; absl::flat_hash_map key_value_stores_{}; @@ -321,6 +238,8 @@ class EngineBuilder { bool enforce_trust_chain_verification_ = true; std::string upstream_tls_sni_; bool enable_http3_ = true; + bool enable_early_data_{true}; + bool scone_enabled_ = false; std::string http3_connection_options_ = ""; std::string http3_client_connection_options_ = ""; // EVMB is to distinguish Envoy Mobile client connections. @@ -339,7 +258,7 @@ class EngineBuilder { std::vector native_filter_chain_; std::vector> dns_preresolve_hostnames_; - absl::optional dns_resolver_config_; + std::optional dns_resolver_config_; std::vector socket_options_; std::vector> runtime_guards_; @@ -370,12 +289,14 @@ class EngineBuilder { int max_time_on_non_default_network_seconds_ = 0; std::string node_id_; - absl::optional node_locality_ = absl::nullopt; - absl::optional node_metadata_ = absl::nullopt; + std::optional node_locality_ = std::nullopt; + std::optional node_metadata_ = std::nullopt; bool enable_stats_collection_ = true; - bool enable_network_change_monitor_ = false; + bool use_worker_thread_{false}; + bool enable_network_change_monitor_{false}; + #ifdef ENVOY_MOBILE_XDS - absl::optional xds_builder_ = absl::nullopt; + std::optional xds_builder_ = std::nullopt; #endif // ENVOY_MOBILE_XDS }; diff --git a/mobile/library/cc/engine_builder_base.h b/mobile/library/cc/engine_builder_base.h new file mode 100644 index 0000000000000..528df9a5706fe --- /dev/null +++ b/mobile/library/cc/engine_builder_base.h @@ -0,0 +1,434 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "absl/debugging/leak_check.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include +#include "envoy/config/listener/v3/listener.pb.h" +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/extensions/filters/http/router/v3/router.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/type/matcher/v3/string.pb.h" +#include "library/cc/engine.h" +#include "library/common/engine_types.h" +#include "library/common/internal_engine.h" +#include "source/common/common/base_logger.h" +#include "source/common/runtime/runtime_features.h" +#include "source/server/options_impl_base.h" + +namespace Envoy { +namespace Platform { + +/** + * Base class template implementing the Curiously Recurring Template Pattern (CRTP) + * to share common bootstrap generation and lifecycle logic between Engine Builders. + * + * How to Subclass: + * Subclasses should derive from this base class passing themselves as the template parameter: + * class MobileEngineBuilder : public Platform::EngineBuilderBase { ... }; + * + * Key CRTP Hooks the Derived Class SHOULD/MUST implement: + * 1. Pre-run and Post-run Lifecycle Hooks: + * - `void preRunSetup(InternalEngine* engine)`: Called inside build() on the engine thread + * prior to starting the event loop. Derived builders should register dynamic key-value stores + * or string accessors here. + * - `void postRunSetup(Engine* engine)`: Called inside build() immediately after starting + * the engine. Used to initialize platform monitors (e.g., NetworkChangeMonitor). + * + * 2. Bootstrap Configuration Hooks: + * - `absl::Status configureNode(envoy::config::core::v3::Node* node)`: + * Configures the Bootstrap's node metadata (e.g., setting app_id, app_version, device_os). + * - `absl::Status configureRouteConfig(envoy::config::route::v3::RouteConfiguration* + * route_config)`: Configures virtual hosts, domains, routes, and early data policies. + * - `absl::Status configureStaticClusters( + * Protobuf::RepeatedPtrField* clusters)`: + * Defines and registers static upstream clusters (e.g., base DFP cluster or mock test + * clusters). + * - `absl::Status configXds(envoy::config::bootstrap::v3::Bootstrap* bootstrap)`: + * Configures dynamic xDS RTDS/CDS layers on the bootstrap. + * + * 3. Optional HTTP Filter Config Overrides: + * - `absl::Status configureHttpFilters( + * std::function + * add_filter)`: Customizes the HttpFilter chain (e.g., AlternateProtocolsCache, Decompressors, + * SocketTagging). + * - `void configureCustomRouterFilter( + * envoy::extensions::filters::http::router::v3::Router& router_config)`: + * Applies custom tweaks to the terminal HTTP router filter. + * - `absl::Status configureTracing(...)`: + * Configures custom tracing providers. + */ +template class EngineBuilderBase { +public: + EngineBuilderBase() : callbacks_(std::make_unique<::Envoy::EngineCallbacks>()) {} + virtual ~EngineBuilderBase() = default; + EngineBuilderBase(EngineBuilderBase&&) = default; + + T& setLogLevel(Logger::Logger::Levels log_level) { + log_level_ = log_level; + return static_cast(*this); + } + + T& setLogger(std::unique_ptr logger) { + logger_ = std::move(logger); + return static_cast(*this); + } + + T& enableLogger(bool logger_on) { + enable_logger_ = logger_on; + return static_cast(*this); + } + + T& setEngineCallbacks(std::unique_ptr callbacks) { + callbacks_ = std::move(callbacks); + return static_cast(*this); + } + + T& setOnEngineRunning(absl::AnyInvocable closure) { + if (!callbacks_) { + callbacks_ = std::make_unique(); + } + callbacks_->on_engine_running_ = std::move(closure); + return static_cast(*this); + } + + T& setOnEngineExit(absl::AnyInvocable closure) { + if (!callbacks_) { + callbacks_ = std::make_unique(); + } + callbacks_->on_exit_ = std::move(closure); + return static_cast(*this); + } + + T& setEventTracker(std::unique_ptr event_tracker) { + event_tracker_ = std::move(event_tracker); + return static_cast(*this); + } + + T& setBufferHighWatermark(size_t high_watermark) { + high_watermark_ = high_watermark; + return static_cast(*this); + } + + T& enableStatsCollection(bool stats_collection_on) { + enable_stats_collection_ = stats_collection_on; + return static_cast(*this); + } + + T& addReloadableRuntimeGuard(std::string guard, bool value) { + reloadable_runtime_guards_.emplace_back(std::move(guard), value); + return static_cast(*this); + } + + T& addRestartRuntimeGuard(std::string guard, bool value) { + restart_runtime_guards_.emplace_back(std::move(guard), value); + return static_cast(*this); + } + + T& setStreamIdleTimeoutSeconds(int stream_idle_timeout_seconds) { + stream_idle_timeout_seconds_ = stream_idle_timeout_seconds; + return static_cast(*this); + } + + T& setRequestTimeoutMilliseconds(int request_timeout_ms) { + request_timeout_ms_ = request_timeout_ms; + return static_cast(*this); + } + + T& setPerConnectionBufferLimitBytes(uint32_t per_connection_buffer_limit_bytes) { + per_connection_buffer_limit_bytes_ = per_connection_buffer_limit_bytes; + return static_cast(*this); + } + + // If true, all HTTP requests are handled on a dedicated worker thread instead of on the Envoy + // main thread which also handles all xDS requests. + // Note: Engine in worker thread model doesn't support platform certificate validation and system + // proxy settings. And these settings will be ignored if worker thread model is enabled. + T& enableWorkerThread(bool use_worker_thread) { + use_worker_thread_ = use_worker_thread; + return static_cast(*this); + } + + T& addApiListener(const envoy::config::listener::v3::Listener& listener) { + custom_listeners_.push_back(listener); + return static_cast(*this); + } + + absl::StatusOr> build() { + auto bootstrap = static_cast(this)->generateBootstrap(); + if (!bootstrap.ok()) { + return bootstrap.status(); + } + + auto envoy_engine = std::make_unique<::Envoy::InternalEngine>( + std::move(callbacks_), std::move(logger_), std::move(event_tracker_), + /*thread_priority=*/std::nullopt, high_watermark_, enable_logger_, useWorkerThread()); + + // Pre-run setup (to be implemented by derived classes if needed) + static_cast(this)->preRunSetup(envoy_engine.get()); + + auto options = std::make_shared(); + options->setConfigProto(std::move(bootstrap.value())); + options->setLogLevel(static_cast(log_level_)); + options->setConcurrency(1); + + envoy_engine->run(options); + + auto engine_wrapper = + std::shared_ptr(new Engine(absl::IgnoreLeak(envoy_engine.release()))); + + static_cast(this)->postRunSetup(engine_wrapper.get()); + + return engine_wrapper; + } + + absl::StatusOr> generateBootstrap() { + auto bootstrap = std::make_unique(); + auto* static_resources = bootstrap->mutable_static_resources(); + + if (custom_listeners_.empty()) { + auto default_listener_or_status = buildDefaultListener("base_api_listener"); + if (!default_listener_or_status.ok()) { + return default_listener_or_status.status(); + } + *static_resources->add_listeners() = std::move(default_listener_or_status.value()); + } else { + // Validate custom listeners + absl::flat_hash_set listener_names; + for (const auto& listener : custom_listeners_) { + if (listener.name().empty()) { + return absl::InvalidArgumentError("Custom listener must have a name."); + } + if (!listener.has_api_listener()) { + return absl::InvalidArgumentError( + absl::StrCat("Custom listener ", listener.name(), " must have an api_listener.")); + } + if (!listener_names.insert(listener.name()).second) { + return absl::InvalidArgumentError( + absl::StrCat("Duplicate listener name: ", listener.name())); + } + } + // Copy custom listeners + for (const auto& listener : custom_listeners_) { + *static_resources->add_listeners() = listener; + } + } + + // Add all clusters to the bootstrap, there should be at least one. + if (auto status = + static_cast(this)->configureStaticClusters(static_resources->mutable_clusters()); + !status.ok()) { + return status; + } + + if (watchdog_.has_value()) { + *bootstrap->mutable_watchdogs() = std::move(watchdog_).value(); + } + + if (auto status = static_cast(this)->configureNode(bootstrap->mutable_node()); + !status.ok()) { + return status; + } + + configureStats(*bootstrap); + configureStaticLayeredRuntime(*bootstrap); + configureListenerManager(*bootstrap); + + if (auto status = static_cast(this)->configXds(bootstrap.get()); !status.ok()) { + return status; + } + + return bootstrap; + } + +protected: + // The default implementation if the derived class does not override it. + absl::Status configureTracing(::envoy::extensions::filters::network::http_connection_manager::v3:: + HttpConnectionManager_Tracing* tracing_config) { + (void)tracing_config; + return absl::OkStatus(); + } + + absl::Status configureHttpFilters( + std::function + add_filter) { + (void)add_filter; + return absl::OkStatus(); + } + + absl::Status configXds(envoy::config::bootstrap::v3::Bootstrap* bootstrap) { + (void)bootstrap; + return absl::OkStatus(); + } + + absl::StatusOr + buildDefaultListener(const std::string& name) { + envoy::config::listener::v3::Listener listener; + listener.set_name(name); + + auto* address = listener.mutable_address(); + address->mutable_socket_address()->set_protocol(::envoy::config::core::v3::SocketAddress::TCP); + address->mutable_socket_address()->set_address("0.0.0.0"); + address->mutable_socket_address()->set_port_value(10000); + listener.mutable_per_connection_buffer_limit_bytes()->set_value( + per_connection_buffer_limit_bytes_); + + ::envoy::extensions::filters::network::http_connection_manager::v3:: + EnvoyMobileHttpConnectionManager api_listener_config; + auto* hcm = api_listener_config.mutable_config(); + hcm->set_stat_prefix("hcm"); + hcm->set_server_header_transformation( + ::envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager:: + PASS_THROUGH); + + ::envoy::extensions::filters::network::http_connection_manager::v3:: + HttpConnectionManager_Tracing tracing; + if (auto status = static_cast(this)->configureTracing(&tracing); !status.ok()) { + return status; + } + if (tracing.ByteSizeLong() > 0) { + *hcm->mutable_tracing() = std::move(tracing); + } + + hcm->mutable_stream_idle_timeout()->set_seconds(stream_idle_timeout_seconds_); + + if (auto status = static_cast(this)->configureRouteConfig(hcm->mutable_route_config()); + !status.ok()) { + return status; + } + + auto add_filter_callback = [hcm]() { return hcm->add_http_filters(); }; + if (auto status = static_cast(this)->configureHttpFilters(add_filter_callback); + !status.ok()) { + return status; + } + + // Add the router filter to the HCM last. + auto* router_filter = hcm->add_http_filters(); + router_filter->set_name("envoy.router"); + ::envoy::extensions::filters::http::router::v3::Router router_config; + static_cast(this)->configureCustomRouterFilter(router_config); + std::ignore = router_filter->mutable_typed_config()->PackFrom(router_config); + + std::ignore = + listener.mutable_api_listener()->mutable_api_listener()->PackFrom(api_listener_config); + + return listener; + } + +protected: + bool useWorkerThread() const { return use_worker_thread_; } + int requestTimeoutMs() const { return request_timeout_ms_; } + + void setWatchdog(envoy::config::bootstrap::v3::Watchdogs watchdog) { + watchdog_ = std::move(watchdog); + } + + void addStatsInclusionPattern(envoy::type::matcher::v3::StringMatcher pattern) { + if (!stats_inclusion_list_.has_value()) { + stats_inclusion_list_.emplace(); + } + *stats_inclusion_list_->add_patterns() = std::move(pattern); + } + +private: + void configureStats(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const { + if (enable_stats_collection_) { + if (stats_inclusion_list_.has_value()) { + auto* list = + bootstrap.mutable_stats_config()->mutable_stats_matcher()->mutable_inclusion_list(); + list->MergeFrom(stats_inclusion_list_.value()); + } + } else { + bootstrap.mutable_stats_config()->mutable_stats_matcher()->set_reject_all(true); + } + bootstrap.mutable_stats_config()->mutable_use_all_default_tags()->set_value(false); + } + + void configureStaticLayeredRuntime(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const { + auto* runtime = bootstrap.mutable_layered_runtime()->add_layers(); + runtime->set_name("static_layer_0"); + Protobuf::Struct envoy_layer; + Protobuf::Struct& runtime_values = + *(*envoy_layer.mutable_fields())["envoy"].mutable_struct_value(); + + if (!reloadable_runtime_guards_.empty()) { + Protobuf::Struct& reloadable_features = + *(*runtime_values.mutable_fields())["reloadable_features"].mutable_struct_value(); + for (const auto& guard_and_value : reloadable_runtime_guards_) { + if (Runtime::RuntimeFeaturesDefaults::get().getFlag(absl::StrJoin( + {"envoy", "reloadable_features", guard_and_value.first}, ".")) == nullptr) { + continue; + } + (*reloadable_features.mutable_fields())[guard_and_value.first].set_bool_value( + guard_and_value.second); + } + } + + if (!restart_runtime_guards_.empty()) { + Protobuf::Struct& restart_features = + *(*runtime_values.mutable_fields())["restart_features"].mutable_struct_value(); + for (const auto& guard_and_value : restart_runtime_guards_) { + if (Runtime::RuntimeFeaturesDefaults::get().getFlag(absl::StrJoin( + {"envoy", "restart_features", guard_and_value.first}, ".")) == nullptr) { + continue; + } + (*restart_features.mutable_fields())[guard_and_value.first].set_bool_value( + guard_and_value.second); + } + } + + (*runtime_values.mutable_fields())["disallow_global_stats"].set_bool_value(true); + + Protobuf::Struct& overload_values = + *(*envoy_layer.mutable_fields())["overload"].mutable_struct_value(); + (*overload_values.mutable_fields())["global_downstream_max_connections"].set_string_value( + "4294967295"); + + runtime->mutable_static_layer()->MergeFrom(envoy_layer); + } + + void configureListenerManager(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const { + envoy::config::bootstrap::v3::ApiListenerManager api; + if (!useWorkerThread()) { + api.set_threading_model(envoy::config::bootstrap::v3::ApiListenerManager::MAIN_THREAD_ONLY); + } else { + api.set_threading_model( + envoy::config::bootstrap::v3::ApiListenerManager::STANDALONE_WORKER_THREAD); + } + auto* listener_manager = bootstrap.mutable_listener_manager(); + std::ignore = listener_manager->mutable_typed_config()->PackFrom(api); + listener_manager->set_name("envoy.listener_manager_impl.api"); + } + + std::optional watchdog_; + std::optional stats_inclusion_list_; + uint32_t per_connection_buffer_limit_bytes_ = 10485760; + int stream_idle_timeout_seconds_ = 15; + int request_timeout_ms_ = 0; + std::vector> reloadable_runtime_guards_; + std::vector> restart_runtime_guards_; + std::vector custom_listeners_; + + // Common fields for InternalEngine + Logger::Logger::Levels log_level_ = Logger::Logger::Levels::info; + std::unique_ptr logger_{nullptr}; + bool enable_logger_{true}; + std::unique_ptr callbacks_; + std::unique_ptr event_tracker_{nullptr}; + std::optional high_watermark_ = std::nullopt; + bool enable_stats_collection_ = true; + bool use_worker_thread_ = false; +}; + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/library/cc/engine_builder_types.cc b/mobile/library/cc/engine_builder_types.cc new file mode 100644 index 0000000000000..7695f7e46fd5a --- /dev/null +++ b/mobile/library/cc/engine_builder_types.cc @@ -0,0 +1,99 @@ +#include "library/cc/engine_builder_types.h" + +#include "absl/strings/str_cat.h" +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/base.pb.h" + +namespace Envoy { +namespace Platform { + +#ifdef ENVOY_MOBILE_XDS +XdsBuilder::XdsBuilder(std::string xds_server_address, const uint32_t xds_server_port) + : xds_server_address_(std::move(xds_server_address)), xds_server_port_(xds_server_port) {} + +XdsBuilder& XdsBuilder::addInitialStreamHeader(std::string header, std::string value) { + envoy::config::core::v3::HeaderValue header_value; + header_value.set_key(std::move(header)); + header_value.set_value(std::move(value)); + xds_initial_grpc_metadata_.emplace_back(std::move(header_value)); + return *this; +} + +XdsBuilder& XdsBuilder::setSslRootCerts(std::string root_certs) { + ssl_root_certs_ = std::move(root_certs); + return *this; +} + +XdsBuilder& XdsBuilder::addRuntimeDiscoveryService(std::string resource_name, + const int timeout_in_seconds) { + rtds_resource_name_ = std::move(resource_name); + rtds_timeout_in_seconds_ = timeout_in_seconds > 0 ? timeout_in_seconds : DefaultXdsTimeout; + return *this; +} + +XdsBuilder& XdsBuilder::addClusterDiscoveryService(std::string cds_resources_locator, + const int timeout_in_seconds) { + enable_cds_ = true; + cds_resources_locator_ = std::move(cds_resources_locator); + cds_timeout_in_seconds_ = timeout_in_seconds > 0 ? timeout_in_seconds : DefaultXdsTimeout; + return *this; +} + +void XdsBuilder::build(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const { + auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); + ads_config->set_transport_api_version(envoy::config::core::v3::ApiVersion::V3); + ads_config->set_set_node_on_first_message_only(true); + ads_config->set_api_type(envoy::config::core::v3::ApiConfigSource::GRPC); + + auto& grpc_service = *ads_config->add_grpc_services(); + grpc_service.mutable_envoy_grpc()->set_cluster_name("base"); + grpc_service.mutable_envoy_grpc()->set_authority( + absl::StrCat(xds_server_address_, ":", xds_server_port_)); + + if (!xds_initial_grpc_metadata_.empty()) { + grpc_service.mutable_initial_metadata()->Assign(xds_initial_grpc_metadata_.begin(), + xds_initial_grpc_metadata_.end()); + } + + if (!rtds_resource_name_.empty()) { + auto* layered_runtime = bootstrap.mutable_layered_runtime(); + auto* layer = layered_runtime->add_layers(); + layer->set_name("rtds_layer"); + auto* rtds_layer = layer->mutable_rtds_layer(); + rtds_layer->set_name(rtds_resource_name_); + auto* rtds_config = rtds_layer->mutable_rtds_config(); + rtds_config->mutable_ads(); + rtds_config->set_resource_api_version(envoy::config::core::v3::ApiVersion::V3); + rtds_config->mutable_initial_fetch_timeout()->set_seconds(rtds_timeout_in_seconds_); + } + + if (enable_cds_) { + auto* cds_config = bootstrap.mutable_dynamic_resources()->mutable_cds_config(); + if (cds_resources_locator_.empty()) { + cds_config->mutable_ads(); + } else { + bootstrap.mutable_dynamic_resources()->set_cds_resources_locator(cds_resources_locator_); + cds_config->mutable_api_config_source()->set_api_type( + envoy::config::core::v3::ApiConfigSource::AGGREGATED_GRPC); + cds_config->mutable_api_config_source()->set_transport_api_version( + envoy::config::core::v3::ApiVersion::V3); + } + cds_config->mutable_initial_fetch_timeout()->set_seconds(cds_timeout_in_seconds_); + cds_config->set_resource_api_version(envoy::config::core::v3::ApiVersion::V3); + bootstrap.add_node_context_params("cluster"); + // Stat prefixes that we use in tests. + auto* list = + bootstrap.mutable_stats_config()->mutable_stats_matcher()->mutable_inclusion_list(); + list->add_patterns()->set_exact("cluster_manager.active_clusters"); + list->add_patterns()->set_exact("cluster_manager.cluster_added"); + list->add_patterns()->set_exact("cluster_manager.cluster_updated"); + list->add_patterns()->set_exact("cluster_manager.cluster_removed"); + // Allow SDS related stats. + list->add_patterns()->mutable_safe_regex()->set_regex("sds\\..*"); + list->add_patterns()->mutable_safe_regex()->set_regex(".*\\.ssl_context_update_by_sds"); + } +} +#endif // ENVOY_MOBILE_XDS + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/library/cc/engine_builder_types.h b/mobile/library/cc/engine_builder_types.h new file mode 100644 index 0000000000000..f0c1bd217a0e0 --- /dev/null +++ b/mobile/library/cc/engine_builder_types.h @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/base.pb.h" + +namespace Envoy { +namespace Platform { + +// Represents the locality information in the Bootstrap's node, as defined in: +// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/base.proto#envoy-v3-api-msg-config-core-v3-locality +struct NodeLocality { + std::string region; + std::string zone; + std::string sub_zone; +}; + +#ifdef ENVOY_MOBILE_XDS +constexpr int DefaultXdsTimeout = 5; + +// Forward declarations so they can be friended by XdsBuilder. +class EngineBuilder; +class MobileEngineBuilder; + +// A class for building the xDS configuration for the Envoy Mobile engine. +// xDS is a protocol for dynamic configuration of Envoy instances, more information can be found in: +// https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol. +// +// This class is typically used as input to the EngineBuilder's/MobileEngineBuilder's setXds() +// method. +class XdsBuilder final { +public: + // `xds_server_address`: the host name or IP address of the xDS management server. The xDS server + // must support the ADS protocol + // (https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration#aggregated-xds-ads). + // `xds_server_port`: the port on which the xDS management server listens for ADS discovery + // requests. + XdsBuilder(std::string xds_server_address, const uint32_t xds_server_port); + + // Adds a header to the initial HTTP metadata headers sent on the gRPC stream. + // + // A common use for the initial metadata headers is for authentication to the xDS management + // server. + // + // For example, if using API keys to authenticate to Traffic Director on GCP (see + // https://cloud.google.com/docs/authentication/api-keys for details), invoke: + // builder.addInitialStreamHeader("x-goog-api-key", api_key_token) + // .addInitialStreamHeader("X-Android-Package", app_package_name) + // .addInitialStreamHeader("X-Android-Cert", sha1_key_fingerprint); + XdsBuilder& addInitialStreamHeader(std::string header, std::string value); + + // Sets the PEM-encoded server root certificates used to negotiate the TLS handshake for the gRPC + // connection. If no root certs are specified, the operating system defaults are used. + XdsBuilder& setSslRootCerts(std::string root_certs); + + // Adds Runtime Discovery Service (RTDS) to the Runtime layers of the Bootstrap configuration, + // to retrieve dynamic runtime configuration via the xDS management server. + // + // `resource_name`: The runtime config resource to subscribe to. + // `timeout_in_seconds`: specifies the `initial_fetch_timeout` field on the + // api.v3.core.ConfigSource. Unlike the ConfigSource default of 15s, we set a default fetch + // timeout value of 5s, to prevent mobile app initialization from stalling. The default + // parameter value may change through the course of experimentation and no assumptions should + // be made of its exact value. + XdsBuilder& addRuntimeDiscoveryService(std::string resource_name, + int timeout_in_seconds = DefaultXdsTimeout); + + // Adds the Cluster Discovery Service (CDS) configuration for retrieving dynamic cluster resources + // via the xDS management server. + // + // `cds_resources_locator`: the xdstp:// URI for subscribing to the cluster resources. + // If not using xdstp, then `cds_resources_locator` should be set to the empty string. + // `timeout_in_seconds`: specifies the `initial_fetch_timeout` field on the + // api.v3.core.ConfigSource. Unlike the ConfigSource default of 15s, we set a default fetch + // timeout value of 5s, to prevent mobile app initialization from stalling. The default + // parameter value may change through the course of experimentation and no assumptions should + // be made of its exact value. + XdsBuilder& addClusterDiscoveryService(std::string cds_resources_locator = "", + int timeout_in_seconds = DefaultXdsTimeout); + +protected: + // Sets the xDS configuration specified on this XdsBuilder instance on the Bootstrap proto + // provided as an input parameter. + // + // This method takes in a modifiable Bootstrap proto pointer because returning a new Bootstrap + // proto would rely on proto's MergeFrom behavior, which can lead to unexpected results in the + // Bootstrap config. + void build(envoy::config::bootstrap::v3::Bootstrap& bootstrap) const; + +private: + // Required so that both Builders can call the XdsBuilder's protected build() method. + friend class EngineBuilder; + friend class MobileEngineBuilder; + + std::string xds_server_address_; + uint32_t xds_server_port_; + std::vector xds_initial_grpc_metadata_; + std::string ssl_root_certs_; + std::string rtds_resource_name_; + int rtds_timeout_in_seconds_ = DefaultXdsTimeout; + bool enable_cds_ = false; + std::string cds_resources_locator_; + int cds_timeout_in_seconds_ = DefaultXdsTimeout; +}; +#endif // ENVOY_MOBILE_XDS + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/library/cc/key_value_store.h b/mobile/library/cc/key_value_store.h index 70b6d5b59d62b..a9008e7d9f917 100644 --- a/mobile/library/cc/key_value_store.h +++ b/mobile/library/cc/key_value_store.h @@ -4,7 +4,7 @@ #include "envoy/common/pure.h" -#include "absl/types/optional.h" +#include #include "library/common/extensions/key_value/platform/c_types.h" namespace Envoy { @@ -21,9 +21,9 @@ struct KeyValueStore : public std::enable_shared_from_this { /** * Returns the value associated with the provided key, if any. * @param key supplies a key to return the value of. - * @return the value, if the key is in the store, absl::nullopt otherwise. + * @return the value, if the key is in the store, std::nullopt otherwise. */ - virtual absl::optional read(const std::string& key) PURE; + virtual std::optional read(const std::string& key) PURE; /** * Adds or updates a key:value pair in the store. diff --git a/mobile/library/cc/mobile_engine_builder.cc b/mobile/library/cc/mobile_engine_builder.cc new file mode 100644 index 0000000000000..8645725d877e8 --- /dev/null +++ b/mobile/library/cc/mobile_engine_builder.cc @@ -0,0 +1,1030 @@ +#include "library/cc/mobile_engine_builder.h" + +#include +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/socket_option.pb.h" +#include "envoy/config/metrics/v3/metrics_service.pb.h" +#include "envoy/extensions/compression/brotli/decompressor/v3/brotli.pb.h" +#include "envoy/extensions/compression/gzip/decompressor/v3/gzip.pb.h" +#include "envoy/extensions/filters/http/alternate_protocols_cache/v3/alternate_protocols_cache.pb.h" +#include "envoy/extensions/filters/http/decompressor/v3/decompressor.pb.h" +#include "envoy/extensions/filters/http/dynamic_forward_proxy/v3/dynamic_forward_proxy.pb.h" +#include "envoy/extensions/filters/http/router/v3/router.pb.h" +#include "envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.pb.h" +#include "envoy/extensions/early_data/v3/default_early_data_policy.pb.h" + +#if defined(__APPLE__) +#include "envoy/extensions/network/dns_resolver/apple/v3/apple_dns_resolver.pb.h" +#endif +#include "envoy/extensions/network/dns_resolver/getaddrinfo/v3/getaddrinfo_dns_resolver.pb.h" +#include "envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.pb.h" +#include "envoy/extensions/transport_sockets/quic/v3/quic_transport.pb.h" +#include "envoy/extensions/transport_sockets/raw_buffer/v3/raw_buffer.pb.h" + +#include "source/common/http/matching/inputs.h" +#include "envoy/config/core/v3/base.pb.h" +#include "source/extensions/clusters/dynamic_forward_proxy/cluster.h" +#include "source/common/runtime/runtime_features.h" +#include "envoy/type/matcher/v3/string.pb.h" + +#include "absl/strings/str_join.h" +#include "absl/strings/str_replace.h" +#include "absl/debugging/leak_check.h" +#include "fmt/core.h" +#include "library/common/internal_engine.h" +#include "library/common/extensions/cert_validator/platform_bridge/platform_bridge.pb.h" +#include "library/common/extensions/filters/http/platform_bridge/filter.pb.h" +#include "library/common/extensions/filters/http/local_error/filter.pb.h" +#include "library/common/extensions/filters/http/network_configuration/filter.pb.h" +#include "library/common/extensions/filters/http/socket_tag/filter.pb.h" +#include "library/common/extensions/key_value/platform/platform.pb.h" +#include "library/common/extensions/quic_packet_writer/platform/platform_packet_writer.pb.h" + +#if defined(__APPLE__) +#include "library/common/network/apple_proxy_resolution.h" +#endif + +namespace Envoy { +namespace Platform { + +MobileEngineBuilder::MobileEngineBuilder() {} + +MobileEngineBuilder& MobileEngineBuilder::setNetworkThreadPriority(int thread_priority) { + network_thread_priority_ = thread_priority; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addConnectTimeoutSeconds(int connect_timeout_seconds) { + connect_timeout_seconds_ = connect_timeout_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addDnsRefreshSeconds(int dns_refresh_seconds) { + dns_refresh_seconds_ = dns_refresh_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addDnsMinRefreshSeconds(int dns_min_refresh_seconds) { + dns_min_refresh_seconds_ = dns_min_refresh_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addDnsFailureRefreshSeconds(int base, int max) { + dns_failure_refresh_seconds_base_ = base; + dns_failure_refresh_seconds_max_ = max; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addDnsQueryTimeoutSeconds(int dns_query_timeout_seconds) { + dns_query_timeout_seconds_ = dns_query_timeout_seconds; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setDisableDnsRefreshOnFailure(bool disable_dns_refresh_on_failure) { + disable_dns_refresh_on_failure_ = disable_dns_refresh_on_failure; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setDisableDnsRefreshOnNetworkChange( + bool disable_dns_refresh_on_network_change) { + disable_dns_refresh_on_network_change_ = disable_dns_refresh_on_network_change; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setDnsNumRetries(uint32_t dns_num_retries) { + dns_num_retries_ = dns_num_retries; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setGetaddrinfoNumThreads(uint32_t num_threads) { + getaddrinfo_num_threads_ = num_threads; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::addDnsPreresolveHostnames(const std::vector& hostnames) { + dns_preresolve_hostnames_.clear(); + for (const std::string& hostname : hostnames) { + dns_preresolve_hostnames_.push_back({hostname /* host */, 443 /* port */}); + } + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setDnsResolver( + const envoy::config::core::v3::TypedExtensionConfig& dns_resolver_config) { + dns_resolver_config_ = dns_resolver_config; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setAdditionalSocketOptions( + const std::vector& socket_options) { + socket_options_ = socket_options; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addMaxConnectionsPerHost(int max_connections_per_host) { + max_connections_per_host_ = max_connections_per_host; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addH2ConnectionKeepaliveIdleIntervalMilliseconds( + int h2_connection_keepalive_idle_interval_milliseconds) { + h2_connection_keepalive_idle_interval_milliseconds_ = + h2_connection_keepalive_idle_interval_milliseconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addH2ConnectionKeepaliveTimeoutSeconds( + int h2_connection_keepalive_timeout_seconds) { + h2_connection_keepalive_timeout_seconds_ = h2_connection_keepalive_timeout_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addKeyValueStore(std::string name, + KeyValueStoreSharedPtr key_value_store) { + key_value_stores_[std::move(name)] = std::move(key_value_store); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setAppVersion(std::string app_version) { + app_version_ = std::move(app_version); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setAppId(std::string app_id) { + app_id_ = std::move(app_id); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setDeviceOs(std::string device_os) { + device_os_ = std::move(device_os); + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setPerTryIdleTimeoutSeconds(int per_try_idle_timeout_seconds) { + per_try_idle_timeout_seconds_ = per_try_idle_timeout_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableGzipDecompression(bool gzip_decompression_on) { + gzip_decompression_filter_ = gzip_decompression_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableBrotliDecompression(bool brotli_decompression_on) { + brotli_decompression_filter_ = brotli_decompression_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableSocketTagging(bool socket_tagging_on) { + socket_tagging_filter_ = socket_tagging_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableHttp3(bool http3_on) { + enable_http3_ = http3_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableEarlyData(bool early_data_on) { + enable_early_data_ = early_data_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableScone(bool enable) { + scone_enabled_ = enable; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addQuicConnectionOption(std::string option) { + quic_connection_options_.push_back(std::move(option)); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addQuicClientConnectionOption(std::string option) { + quic_client_connection_options_.push_back(std::move(option)); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setHttp3ConnectionOptions(std::string options) { + http3_connection_options_ = std::move(options); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setHttp3ClientConnectionOptions(std::string options) { + http3_client_connection_options_ = std::move(options); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addQuicHint(std::string host, int port) { + quic_hints_.emplace_back(std::move(host), port); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addQuicCanonicalSuffix(std::string suffix) { + quic_suffixes_.emplace_back(std::move(suffix)); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setNumTimeoutsToTriggerPortMigration(int num_timeouts) { + num_timeouts_to_trigger_port_migration_ = num_timeouts; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableInterfaceBinding(bool interface_binding_on) { + enable_interface_binding_ = interface_binding_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setUdpSocketReceiveBufferSize(int32_t size) { + udp_socket_receive_buffer_size_ = size; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setUdpSocketSendBufferSize(int32_t size) { + udp_socket_send_buffer_size_ = size; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::enableDrainPostDnsRefresh(bool drain_post_dns_refresh_on) { + enable_drain_post_dns_refresh_ = drain_post_dns_refresh_on; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::enforceTrustChainVerification(bool trust_chain_verification_on) { + enforce_trust_chain_verification_ = trust_chain_verification_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setUpstreamTlsSni(std::string sni) { + upstream_tls_sni_ = std::move(sni); + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setQuicConnectionIdleTimeoutSeconds(int quic_connection_idle_timeout_seconds) { + quic_connection_idle_timeout_seconds_ = quic_connection_idle_timeout_seconds; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setKeepAliveInitialIntervalMilliseconds(int keepalive_initial_interval_ms) { + keepalive_initial_interval_ms_ = keepalive_initial_interval_ms; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setMaxConcurrentStreams(int max_concurrent_streams) { + max_concurrent_streams_ = max_concurrent_streams; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enablePlatformCertificatesValidation( + bool platform_certificates_validation_on) { + if (useWorkerThread()) { + return *this; + } + platform_certificates_validation_on_ = platform_certificates_validation_on; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableDnsCache(bool dns_cache_on, + int save_interval_seconds) { + dns_cache_on_ = dns_cache_on; + dns_cache_save_interval_seconds_ = save_interval_seconds; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addStringAccessor(std::string name, + StringAccessorSharedPtr accessor) { + string_accessors_[std::move(name)] = std::move(accessor); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addNativeFilter(std::string name, + std::string typed_config) { + native_filter_chain_.emplace_back(NativeFilterConfig(std::move(name), std::move(typed_config))); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addNativeFilter(const std::string& name, + const Protobuf::Any& typed_config) { + native_filter_chain_.push_back(NativeFilterConfig(name, typed_config)); + return *this; +} + +#if defined(__APPLE__) +MobileEngineBuilder& +MobileEngineBuilder::enableNetworkChangeMonitor(bool network_change_monitor_on) { + enable_network_change_monitor_ = network_change_monitor_on; + return *this; +} +#endif + +std::string MobileEngineBuilder::nativeNameToConfig(absl::string_view name) { +#ifdef ENVOY_ENABLE_FULL_PROTOS + return absl::StrCat("[type.googleapis.com/" + "envoymobile.extensions.filters.http.platform_bridge.PlatformBridge] {" + "platform_filter_name: \"", + name, "\" }"); +#else + envoymobile::extensions::filters::http::platform_bridge::PlatformBridge proto_config; + proto_config.set_platform_filter_name(name); + std::string ret; + std::ignore = proto_config.SerializeToString(&ret); + Protobuf::Any any_config; + any_config.set_type_url( + "type.googleapis.com/envoymobile.extensions.filters.http.platform_bridge.PlatformBridge"); + any_config.set_value(ret); + std::ignore = any_config.SerializeToString(&ret); + return ret; +#endif +} + +MobileEngineBuilder& MobileEngineBuilder::addPlatformFilter(const std::string& name) { + addNativeFilter("envoy.filters.http.platform_bridge", nativeNameToConfig(name)); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::enableWorkerThread(bool use_worker_thread) { + Platform::EngineBuilderBase::enableWorkerThread(use_worker_thread); + if (useWorkerThread()) { + platform_certificates_validation_on_ = false; +#ifdef __APPLE__ + respect_system_proxy_settings_ = false; +#endif + } + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::addRuntimeGuard(std::string guard, bool value) { + addReloadableRuntimeGuard(std::move(guard), value); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setNodeId(std::string node_id) { + node_id_ = std::move(node_id); + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setNodeLocality(std::string region, std::string zone, + std::string sub_zone) { + node_locality_ = {std::move(region), std::move(zone), std::move(sub_zone)}; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setNodeMetadata(Protobuf::Struct node_metadata) { + node_metadata_ = std::move(node_metadata); + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setUseQuicPlatformPacketWriter(bool use_quic_platform_packet_writer) { + use_quic_platform_packet_writer_ = use_quic_platform_packet_writer; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::enableQuicConnectionMigration(bool quic_connection_migration_on) { + enable_quic_connection_migration_ = quic_connection_migration_on; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setMigrateIdleQuicConnection(bool migrate_idle_quic_connection) { + migrate_idle_quic_connection_ = migrate_idle_quic_connection; + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setMaxIdleTimeBeforeQuicMigrationSeconds( + int max_idle_time_before_quic_migration) { + max_idle_time_before_quic_migration_seconds_ = max_idle_time_before_quic_migration; + return *this; +} + +MobileEngineBuilder& +MobileEngineBuilder::setMaxTimeOnNonDefaultNetworkSeconds(int max_time_on_non_default_network) { + max_time_on_non_default_network_seconds_ = max_time_on_non_default_network; + return *this; +} + +#if defined(__APPLE__) +MobileEngineBuilder& MobileEngineBuilder::respectSystemProxySettings(bool value, + int refresh_interval_secs) { + if (useWorkerThread()) { + return *this; + } + respect_system_proxy_settings_ = value; + if (refresh_interval_secs > 0) { + proxy_settings_refresh_interval_secs_ = refresh_interval_secs; + } + return *this; +} + +MobileEngineBuilder& MobileEngineBuilder::setIosNetworkServiceType(int ios_network_service_type) { + ios_network_service_type_ = ios_network_service_type; + return *this; +} +#endif + +#ifdef ENVOY_MOBILE_XDS + +MobileEngineBuilder& MobileEngineBuilder::setXds(XdsBuilder xds_builder) { + xds_builder_ = std::move(xds_builder); + dns_preresolve_hostnames_.push_back( + {xds_builder_->xds_server_address_ /* host */, xds_builder_->xds_server_port_ /* port */}); + return *this; +} +#endif // ENVOY_MOBILE_XDS + +absl::Status MobileEngineBuilder::configureRouteConfig( + envoy::config::route::v3::RouteConfiguration* route_config) { + route_config->set_name("api_router"); + + auto* api_service = route_config->add_virtual_hosts(); + api_service->set_name("api"); + api_service->set_include_attempt_count_in_response(true); + api_service->add_domains("*"); + + auto* route = api_service->add_routes(); + route->mutable_match()->set_prefix("/"); + route->add_request_headers_to_remove("x-forwarded-proto"); + route->add_request_headers_to_remove("x-envoy-mobile-cluster"); + route->mutable_per_request_buffer_limit_bytes()->set_value(4096); + auto* route_to = route->mutable_route(); + route_to->set_cluster_header("x-envoy-mobile-cluster"); + if (requestTimeoutMs() > 0) { + route_to->mutable_timeout()->set_nanos((requestTimeoutMs() % 1000) * 1000000); + route_to->mutable_timeout()->set_seconds(requestTimeoutMs() / 1000); + } else { + route_to->mutable_timeout()->set_seconds(0); + } + route_to->mutable_retry_policy()->mutable_per_try_idle_timeout()->set_seconds( + per_try_idle_timeout_seconds_); + auto* backoff = route_to->mutable_retry_policy()->mutable_retry_back_off(); + backoff->mutable_base_interval()->set_nanos(250000000); + backoff->mutable_max_interval()->set_seconds(60); + + if (!enable_early_data_) { + auto* early_data = route_to->mutable_early_data_policy(); + early_data->set_name("envoy.route.early_data_policy.default"); + ::envoy::extensions::early_data::v3::DefaultEarlyDataPolicy config; + std::ignore = early_data->mutable_typed_config()->PackFrom(config); + } + return absl::OkStatus(); +} + +absl::Status MobileEngineBuilder::configureHttpFilters( + std::function + add_filter) { + for (auto filter = native_filter_chain_.rbegin(); filter != native_filter_chain_.rend(); + ++filter) { + auto* native_filter = add_filter(); + native_filter->set_name(filter->name_); + if (!filter->textproto_typed_config_.empty()) { +#if ENVOY_ENABLE_FULL_PROTOS + std::ignore = Protobuf::TextFormat::ParseFromString((*filter).textproto_typed_config_, + native_filter->mutable_typed_config()); + RELEASE_ASSERT(!native_filter->typed_config().DebugString().empty(), + "Failed to parse: " + (*filter).textproto_typed_config_); +#else + RELEASE_ASSERT( + native_filter->mutable_typed_config()->ParseFromString((*filter).textproto_typed_config_), + "Failed to parse binary proto: " + (*filter).textproto_typed_config_); +#endif // !ENVOY_ENABLE_FULL_PROTOS + } else { + *native_filter->mutable_typed_config() = filter->typed_config_; + } + } + + if (enable_http3_) { + envoy::extensions::filters::http::alternate_protocols_cache::v3::FilterConfig cache_config; + auto* cache_filter = add_filter(); + cache_filter->set_name("alternate_protocols_cache"); + std::ignore = cache_filter->mutable_typed_config()->PackFrom(cache_config); + } + + if (gzip_decompression_filter_) { + envoy::extensions::compression::gzip::decompressor::v3::Gzip gzip_config; + gzip_config.mutable_window_bits()->set_value(15); + envoy::extensions::filters::http::decompressor::v3::Decompressor decompressor_config; + decompressor_config.mutable_decompressor_library()->set_name("gzip"); + std::ignore = + decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( + gzip_config); + auto* common_request = + decompressor_config.mutable_request_direction_config()->mutable_common_config(); + common_request->mutable_enabled()->mutable_default_value(); + common_request->mutable_enabled()->set_runtime_key("request_decompressor_enabled"); + decompressor_config.mutable_response_direction_config() + ->mutable_common_config() + ->set_ignore_no_transform_header(true); + auto* gzip_filter = add_filter(); + gzip_filter->set_name("envoy.filters.http.decompressor"); + std::ignore = gzip_filter->mutable_typed_config()->PackFrom(decompressor_config); + } + if (brotli_decompression_filter_) { + envoy::extensions::compression::brotli::decompressor::v3::Brotli brotli_config; + envoy::extensions::filters::http::decompressor::v3::Decompressor decompressor_config; + decompressor_config.mutable_decompressor_library()->set_name("text_optimized"); + std::ignore = + decompressor_config.mutable_decompressor_library()->mutable_typed_config()->PackFrom( + brotli_config); + auto* common_request = + decompressor_config.mutable_request_direction_config()->mutable_common_config(); + common_request->mutable_enabled()->mutable_default_value(); + common_request->mutable_enabled()->set_runtime_key("request_decompressor_enabled"); + decompressor_config.mutable_response_direction_config() + ->mutable_common_config() + ->set_ignore_no_transform_header(true); + auto* brotli_filter = add_filter(); + brotli_filter->set_name("envoy.filters.http.decompressor"); + std::ignore = brotli_filter->mutable_typed_config()->PackFrom(decompressor_config); + } + if (socket_tagging_filter_) { + envoymobile::extensions::filters::http::socket_tag::SocketTag tag_config; + auto* tag_filter = add_filter(); + tag_filter->set_name("envoy.filters.http.socket_tag"); + std::ignore = tag_filter->mutable_typed_config()->PackFrom(tag_config); + } + + envoymobile::extensions::filters::http::network_configuration::NetworkConfiguration + network_config; + network_config.set_enable_drain_post_dns_refresh(enable_drain_post_dns_refresh_); + network_config.set_enable_interface_binding(enable_interface_binding_); + auto* network_filter = add_filter(); + network_filter->set_name("envoy.filters.http.network_configuration"); + std::ignore = network_filter->mutable_typed_config()->PackFrom(network_config); + + envoymobile::extensions::filters::http::local_error::LocalError local_config; + auto* local_filter = add_filter(); + local_filter->set_name("envoy.filters.http.local_error"); + std::ignore = local_filter->mutable_typed_config()->PackFrom(local_config); + + envoy::extensions::filters::http::dynamic_forward_proxy::v3::FilterConfig dfp_config; + configureDnsCache(dfp_config.mutable_dns_cache_config()); + auto* dfp_filter = add_filter(); + dfp_filter->set_name("envoy.filters.http.dynamic_forward_proxy"); + std::ignore = dfp_filter->mutable_typed_config()->PackFrom(dfp_config); + return absl::OkStatus(); +} + +void MobileEngineBuilder::configureDnsCache( + envoy::extensions::common::dynamic_forward_proxy::v3::DnsCacheConfig* dns_cache_config) const { + dns_cache_config->set_name("base_dns_cache"); + dns_cache_config->set_dns_lookup_family(envoy::config::cluster::v3::Cluster::ALL); + dns_cache_config->mutable_host_ttl()->set_seconds(86400); + dns_cache_config->mutable_dns_min_refresh_rate()->set_seconds(dns_min_refresh_seconds_); + dns_cache_config->mutable_dns_refresh_rate()->set_seconds(dns_refresh_seconds_); + dns_cache_config->mutable_dns_failure_refresh_rate()->mutable_base_interval()->set_seconds( + dns_failure_refresh_seconds_base_); + dns_cache_config->mutable_dns_failure_refresh_rate()->mutable_max_interval()->set_seconds( + dns_failure_refresh_seconds_max_); + dns_cache_config->mutable_dns_query_timeout()->set_seconds(dns_query_timeout_seconds_); + dns_cache_config->set_disable_dns_refresh_on_failure(disable_dns_refresh_on_failure_); + if (dns_cache_on_) { + envoymobile::extensions::key_value::platform::PlatformKeyValueStoreConfig kv_config; + kv_config.set_key("dns_persistent_cache"); + kv_config.mutable_save_interval()->set_seconds(dns_cache_save_interval_seconds_); + kv_config.set_max_entries(100); + dns_cache_config->mutable_key_value_config()->mutable_config()->set_name( + "envoy.key_value.platform"); + std::ignore = dns_cache_config->mutable_key_value_config() + ->mutable_config() + ->mutable_typed_config() + ->PackFrom(kv_config); + } + + if (dns_resolver_config_.has_value()) { + *dns_cache_config->mutable_typed_dns_resolver_config() = *dns_resolver_config_; + } else { +#if defined(__APPLE__) + envoy::extensions::network::dns_resolver::apple::v3::AppleDnsResolverConfig resolver_config; + dns_cache_config->mutable_typed_dns_resolver_config()->set_name( + "envoy.network.dns_resolver.apple"); + std::ignore = + dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( + resolver_config); +#else + envoy::extensions::network::dns_resolver::getaddrinfo::v3::GetAddrInfoDnsResolverConfig + resolver_config; + if (dns_num_retries_.has_value()) { + resolver_config.mutable_num_retries()->set_value(*dns_num_retries_); + } + resolver_config.mutable_num_resolver_threads()->set_value(getaddrinfo_num_threads_); + dns_cache_config->mutable_typed_dns_resolver_config()->set_name( + "envoy.network.dns_resolver.getaddrinfo"); + std::ignore = + dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( + resolver_config); +#endif + } + + for (const auto& [host, port] : dns_preresolve_hostnames_) { + envoy::config::core::v3::SocketAddress* address = dns_cache_config->add_preresolve_hostnames(); + address->set_address(host); + address->set_port_value(port); + } +} + +absl::Status MobileEngineBuilder::configureStaticClusters( + Protobuf::RepeatedPtrField* clusters) { + envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext tls_socket; + if (!upstream_tls_sni_.empty()) { + tls_socket.set_sni(upstream_tls_sni_); + } + tls_socket.mutable_common_tls_context()->mutable_tls_params()->set_tls_maximum_protocol_version( + envoy::extensions::transport_sockets::tls::v3::TlsParameters::TLSv1_3); + auto* validation = tls_socket.mutable_common_tls_context()->mutable_validation_context(); + if (enforce_trust_chain_verification_) { + validation->set_trust_chain_verification(envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::VERIFY_TRUST_CHAIN); + } else { + validation->set_trust_chain_verification(envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::ACCEPT_UNTRUSTED); + } + + if (platform_certificates_validation_on_) { + envoy_mobile::extensions::cert_validator::platform_bridge::PlatformBridgeCertValidator + validator; + if (network_thread_priority_.has_value()) { + validator.mutable_thread_priority()->set_value(*network_thread_priority_); + } + validation->mutable_custom_validator_config()->set_name( + "envoy_mobile.cert_validator.platform_bridge_cert_validator"); + std::ignore = + validation->mutable_custom_validator_config()->mutable_typed_config()->PackFrom(validator); + } else { + std::string certs; +#ifdef ENVOY_MOBILE_XDS + if (xds_builder_ && !xds_builder_->ssl_root_certs_.empty()) { + certs = xds_builder_->ssl_root_certs_; + } +#endif // ENVOY_MOBILE_XDS + if (certs.empty()) { + const char* inline_certs = "" +#ifndef EXCLUDE_CERTIFICATES +#include "library/common/config/certificates.inc" +#endif + ""; + certs = inline_certs; + absl::StrReplaceAll({{"\n ", "\n"}}, &certs); + } + if (!certs.empty()) { + validation->mutable_trusted_ca()->set_inline_string(certs); + } + } + envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport + ssl_proxy_socket; + ssl_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.tls"); + std::ignore = + ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); + + envoy::extensions::upstreams::http::v3::HttpProtocolOptions h2_protocol_options; + h2_protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + + envoy::config::cluster::v3::Cluster* base_cluster = clusters->Add(); + envoy::extensions::clusters::dynamic_forward_proxy::v3::ClusterConfig base_cluster_config; + envoy::config::cluster::v3::Cluster::CustomClusterType base_cluster_type; + configureDnsCache(base_cluster_config.mutable_dns_cache_config()); + base_cluster_type.set_name("envoy.clusters.dynamic_forward_proxy"); + std::ignore = base_cluster_type.mutable_typed_config()->PackFrom(base_cluster_config); + + auto* upstream_opts = base_cluster->mutable_upstream_connection_options(); + upstream_opts->set_set_local_interface_name_on_upstream_connections(true); + upstream_opts->mutable_tcp_keepalive()->mutable_keepalive_interval()->set_value(5); + upstream_opts->mutable_tcp_keepalive()->mutable_keepalive_probes()->set_value(1); + upstream_opts->mutable_tcp_keepalive()->mutable_keepalive_time()->set_value(10); + + auto* circuit_breaker_settings = base_cluster->mutable_circuit_breakers(); + auto* breaker1 = circuit_breaker_settings->add_thresholds(); + breaker1->set_priority(envoy::config::core::v3::RoutingPriority::DEFAULT); + breaker1->mutable_retry_budget()->mutable_budget_percent()->set_value(100); + breaker1->mutable_retry_budget()->mutable_min_retry_concurrency()->set_value(0xffffffff); + auto* breaker2 = circuit_breaker_settings->add_per_host_thresholds(); + breaker2->set_priority(envoy::config::core::v3::RoutingPriority::DEFAULT); + breaker2->mutable_max_connections()->set_value(max_connections_per_host_); + + envoy::extensions::upstreams::http::v3::HttpProtocolOptions alpn_options; + alpn_options.mutable_upstream_http_protocol_options()->set_auto_sni(true); + alpn_options.mutable_upstream_http_protocol_options()->set_auto_san_validation(true); + auto* h2_options = alpn_options.mutable_auto_config()->mutable_http2_protocol_options(); + if (h2_connection_keepalive_idle_interval_milliseconds_ > 1000) { + h2_options->mutable_connection_keepalive()->mutable_connection_idle_interval()->set_seconds( + h2_connection_keepalive_idle_interval_milliseconds_ / 1000); + } else { + h2_options->mutable_connection_keepalive()->mutable_connection_idle_interval()->set_nanos( + h2_connection_keepalive_idle_interval_milliseconds_ * 1000 * 1000); + } + h2_options->mutable_connection_keepalive()->mutable_timeout()->set_seconds( + h2_connection_keepalive_timeout_seconds_); + h2_options->mutable_max_concurrent_streams()->set_value(100); + h2_options->mutable_initial_stream_window_size()->set_value(initial_stream_window_size_); + h2_options->mutable_initial_connection_window_size()->set_value(initial_connection_window_size_); + if (max_concurrent_streams_ > 0) { + h2_options->mutable_max_concurrent_streams()->set_value(max_concurrent_streams_); + } + + envoy::extensions::http::header_formatters::preserve_case::v3::PreserveCaseFormatterConfig + preserve_case_config; + preserve_case_config.set_forward_reason_phrase(false); + preserve_case_config.set_formatter_type_on_envoy_headers( + envoy::extensions::http::header_formatters::preserve_case::v3::PreserveCaseFormatterConfig:: + DEFAULT); + + auto* h1_options = alpn_options.mutable_auto_config()->mutable_http_protocol_options(); + auto* formatter = h1_options->mutable_header_key_format()->mutable_stateful_formatter(); + formatter->set_name("preserve_case"); + std::ignore = formatter->mutable_typed_config()->PackFrom(preserve_case_config); + + // Base cluster + base_cluster->set_name("base"); + base_cluster->mutable_connect_timeout()->set_seconds(connect_timeout_seconds_); + base_cluster->set_lb_policy(envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED); + std::ignore = (*base_cluster->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(alpn_options); + base_cluster->mutable_cluster_type()->CopyFrom(base_cluster_type); + base_cluster->mutable_transport_socket()->set_name("envoy.transport_sockets.http_11_proxy"); + std::ignore = + base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(ssl_proxy_socket); + + // Base clear-text cluster set up + envoy::extensions::transport_sockets::raw_buffer::v3::RawBuffer raw_buffer; + envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport + cleartext_proxy_socket; + std::ignore = cleartext_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom( + raw_buffer); + cleartext_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.raw_buffer"); + envoy::extensions::upstreams::http::v3::HttpProtocolOptions h1_protocol_options; + h1_protocol_options.mutable_upstream_http_protocol_options()->set_auto_sni(true); + h1_protocol_options.mutable_upstream_http_protocol_options()->set_auto_san_validation(true); + h1_protocol_options.mutable_explicit_http_config()->mutable_http_protocol_options()->CopyFrom( + *alpn_options.mutable_auto_config()->mutable_http_protocol_options()); + + // Base clear-text cluster. + envoy::config::cluster::v3::Cluster* base_clear = clusters->Add(); + base_clear->set_name("base_clear"); + base_clear->mutable_connect_timeout()->set_seconds(connect_timeout_seconds_); + base_clear->set_lb_policy(envoy::config::cluster::v3::Cluster::CLUSTER_PROVIDED); + base_clear->mutable_cluster_type()->CopyFrom(base_cluster_type); + base_clear->mutable_transport_socket()->set_name("envoy.transport_sockets.http_11_proxy"); + std::ignore = base_clear->mutable_transport_socket()->mutable_typed_config()->PackFrom( + cleartext_proxy_socket); + base_clear->mutable_upstream_connection_options()->CopyFrom( + *base_cluster->mutable_upstream_connection_options()); + base_clear->mutable_circuit_breakers()->CopyFrom(*base_cluster->mutable_circuit_breakers()); + std::ignore = (*base_clear->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(h1_protocol_options); + + // Edit and re-pack + tls_socket.mutable_common_tls_context()->add_alpn_protocols("h2"); + std::ignore = + ssl_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_socket); + + // Edit base cluster to be an HTTP/3 cluster. + if (enable_http3_) { + envoy::extensions::transport_sockets::quic::v3::QuicUpstreamTransport h3_inner_socket; + tls_socket.mutable_common_tls_context()->mutable_alpn_protocols()->Clear(); + h3_inner_socket.mutable_upstream_tls_context()->CopyFrom(tls_socket); + envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport + h3_proxy_socket; + std::ignore = h3_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom( + h3_inner_socket); + h3_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.quic"); + + auto* quic_protocol_options = alpn_options.mutable_auto_config() + ->mutable_http3_protocol_options() + ->mutable_quic_protocol_options(); + if (!quic_connection_options_.empty()) { + quic_protocol_options->set_connection_options(absl::StrJoin(quic_connection_options_, ",")); + } else { + quic_protocol_options->set_connection_options(http3_connection_options_); + } + if (!quic_client_connection_options_.empty()) { + quic_protocol_options->set_client_connection_options( + absl::StrJoin(quic_client_connection_options_, ",")); + } else { + quic_protocol_options->set_client_connection_options(http3_client_connection_options_); + } + quic_protocol_options->mutable_initial_stream_window_size()->set_value( + initial_stream_window_size_); + quic_protocol_options->mutable_initial_connection_window_size()->set_value( + initial_connection_window_size_); + quic_protocol_options->mutable_idle_network_timeout()->set_seconds( + quic_connection_idle_timeout_seconds_); + if (num_timeouts_to_trigger_port_migration_ > 0) { + quic_protocol_options->mutable_num_timeouts_to_trigger_port_migration()->set_value( + num_timeouts_to_trigger_port_migration_); + } + if (keepalive_initial_interval_ms_ > 0) { + quic_protocol_options->mutable_connection_keepalive()->mutable_initial_interval()->set_nanos( + keepalive_initial_interval_ms_ * 1000 * 1000); + } + if (max_concurrent_streams_ > 0) { + quic_protocol_options->mutable_max_concurrent_streams()->set_value(max_concurrent_streams_); + } + if (enable_quic_connection_migration_) { + auto* migration_setting = quic_protocol_options->mutable_connection_migration(); + if (migrate_idle_quic_connection_) { + auto* migrate_idle_connections = migration_setting->mutable_migrate_idle_connections(); + if (max_idle_time_before_quic_migration_seconds_ > 0) { + migrate_idle_connections->mutable_max_idle_time_before_migration()->set_seconds( + max_idle_time_before_quic_migration_seconds_); + } + } + if (max_time_on_non_default_network_seconds_ > 0) { + migration_setting->mutable_max_time_on_non_default_network()->set_seconds( + max_time_on_non_default_network_seconds_); + } + } + + if (scone_enabled_) { + quic_protocol_options->mutable_enable_scone()->set_value(true); + } + + if (use_quic_platform_packet_writer_ || enable_quic_connection_migration_) { + envoy_mobile::extensions::quic_packet_writer::platform::QuicPlatformPacketWriterConfig + writer_config; + std::ignore = + quic_protocol_options->mutable_client_packet_writer()->mutable_typed_config()->PackFrom( + writer_config); + quic_protocol_options->mutable_client_packet_writer()->set_name( + "envoy.quic.packet_writer.platform"); + } + + alpn_options.mutable_auto_config()->mutable_alternate_protocols_cache_options()->set_name( + "default_alternate_protocols_cache"); + for (const auto& [host, port] : quic_hints_) { + auto* entry = alpn_options.mutable_auto_config() + ->mutable_alternate_protocols_cache_options() + ->add_prepopulated_entries(); + entry->set_hostname(host); + entry->set_port(port); + } + for (const auto& suffix : quic_suffixes_) { + alpn_options.mutable_auto_config() + ->mutable_alternate_protocols_cache_options() + ->add_canonical_suffixes(suffix); + } + + std::ignore = + base_cluster->mutable_transport_socket()->mutable_typed_config()->PackFrom(h3_proxy_socket); + std::ignore = (*base_cluster->mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(alpn_options); + + // Set the upstream connections UDP socket receive buffer size. The operating system defaults + // are usually too small for QUIC. + envoy::config::core::v3::SocketOption* udp_rcv_buf_sock_opt = + base_cluster->mutable_upstream_bind_config()->add_socket_options(); + udp_rcv_buf_sock_opt->set_name(SO_RCVBUF); + udp_rcv_buf_sock_opt->set_level(SOL_SOCKET); + udp_rcv_buf_sock_opt->set_int_value(udp_socket_receive_buffer_size_); + // Only apply the socket option to the datagram socket. + udp_rcv_buf_sock_opt->mutable_type()->mutable_datagram(); + udp_rcv_buf_sock_opt->set_description( + absl::StrCat("UDP SO_RCVBUF = ", udp_socket_receive_buffer_size_, " bytes")); + + envoy::config::core::v3::SocketOption* udp_snd_buf_sock_opt = + base_cluster->mutable_upstream_bind_config()->add_socket_options(); + udp_snd_buf_sock_opt->set_name(SO_SNDBUF); + udp_snd_buf_sock_opt->set_level(SOL_SOCKET); + udp_snd_buf_sock_opt->set_int_value(udp_socket_send_buffer_size_); + // Only apply the socket option to the datagram socket. + udp_snd_buf_sock_opt->mutable_type()->mutable_datagram(); + udp_snd_buf_sock_opt->set_description( + absl::StrCat("UDP SO_SNDBUF = ", udp_socket_send_buffer_size_, " bytes")); + // Set the network service type on iOS, if supplied. +#if defined(__APPLE__) + if (ios_network_service_type_ > 0) { + envoy::config::core::v3::SocketOption* net_svc_sock_opt = + base_cluster->mutable_upstream_bind_config()->add_socket_options(); + net_svc_sock_opt->set_name(SO_NET_SERVICE_TYPE); + net_svc_sock_opt->set_level(SOL_SOCKET); + net_svc_sock_opt->set_int_value(ios_network_service_type_); + net_svc_sock_opt->set_description( + absl::StrCat("SO_NET_SERVICE_TYPE = ", ios_network_service_type_)); + } +#endif + for (const auto& socket_option : socket_options_) { + envoy::config::core::v3::SocketOption* sock_opt = + base_cluster->mutable_upstream_bind_config()->add_socket_options(); + sock_opt->CopyFrom(socket_option); + } + } + return absl::OkStatus(); +} + +void MobileEngineBuilder::addStatsInclusionStringMatchers() { + auto add_prefix = [this](const std::string& prefix) { + envoy::type::matcher::v3::StringMatcher matcher; + matcher.set_prefix(prefix); + addStatsInclusionPattern(std::move(matcher)); + }; + auto add_regex = [this](const std::string& regex) { + envoy::type::matcher::v3::StringMatcher matcher; + matcher.mutable_safe_regex()->set_regex(regex); + addStatsInclusionPattern(std::move(matcher)); + }; + add_prefix("cluster.base.upstream_rq_"); + add_prefix("cluster.stats.upstream_rq_"); + add_prefix("cluster.base.upstream_cx_"); + add_prefix("cluster.stats.upstream_cx_"); + add_regex("^cluster\\.base\\.http2\\.keepalive_timeout$"); + add_regex("^cluster\\.base\\.upstream_http3_broken$"); + add_regex("^cluster\\.stats\\.http2\\.keepalive_timeout$"); + add_prefix("http.hcm.downstream_rq_"); + add_prefix("http.hcm.decompressor."); + add_prefix("pulse."); + add_prefix("runtime.load_success"); + add_prefix("dns_cache"); + add_regex("^vhost\\.[\\w]+\\.vcluster\\.[\\w]+?\\.upstream_rq_(?:[12345]xx|[3-5][0-9][0-9]|retry|" + "total)"); + add_regex(".*quic_connection_close_error_code.*"); + add_regex(".*quic_reset_stream_error_code.*"); +} + +void MobileEngineBuilder::addWatchdog() { + envoy::config::bootstrap::v3::Watchdogs watchdog; + watchdog.mutable_main_thread_watchdog()->mutable_megamiss_timeout()->set_seconds(60); + watchdog.mutable_main_thread_watchdog()->mutable_miss_timeout()->set_seconds(60); + watchdog.mutable_worker_watchdog()->mutable_megamiss_timeout()->set_seconds(60); + watchdog.mutable_worker_watchdog()->mutable_miss_timeout()->set_seconds(60); + setWatchdog(std::move(watchdog)); +} + +absl::Status MobileEngineBuilder::configureNode(envoy::config::core::v3::Node* node) { + node->set_id(node_id_.empty() ? "envoy-mobile" : node_id_); + node->set_cluster("envoy-mobile"); + if (node_locality_ && !node_locality_->region.empty()) { + node->mutable_locality()->set_region(node_locality_->region); + node->mutable_locality()->set_zone(node_locality_->zone); + node->mutable_locality()->set_sub_zone(node_locality_->sub_zone); + } + if (node_metadata_.has_value()) { + *node->mutable_metadata() = *node_metadata_; + } + Protobuf::Struct& metadata = *node->mutable_metadata(); + (*metadata.mutable_fields())["app_id"].set_string_value(app_id_); + (*metadata.mutable_fields())["app_version"].set_string_value(app_version_); + (*metadata.mutable_fields())["device_os"].set_string_value(device_os_); + + return absl::OkStatus(); +} + +absl::Status MobileEngineBuilder::configXds(envoy::config::bootstrap::v3::Bootstrap* bootstrap) { + (void)bootstrap; +#ifdef ENVOY_MOBILE_XDS + if (xds_builder_) { + xds_builder_->build(*bootstrap); + } +#endif // ENVOY_MOBILE_XDS + return absl::OkStatus(); +} + +EngineSharedPtr MobileEngineBuilder::build() { + return Platform::EngineBuilderBase::build().value(); +} + +void MobileEngineBuilder::preRunSetup(InternalEngine* engine) { + engine->disableDnsRefreshOnNetworkChange(disable_dns_refresh_on_network_change_); + for (const auto& [name, store] : key_value_stores_) { + // TODO(goaway): This leaks, but it's tied to the life of the engine. + if (!Api::External::retrieveApi(name, true)) { + auto* api = new envoy_kv_store(); + *api = store->asEnvoyKeyValueStore(); + Envoy::Api::External::registerApi(name.c_str(), api); + } + } + + for (const auto& [name, accessor] : string_accessors_) { + // TODO(RyanTheOptimist): This leaks, but it's tied to the life of the engine. + if (!Api::External::retrieveApi(name, true)) { + auto* api = new envoy_string_accessor(); + *api = StringAccessor::asEnvoyStringAccessor(accessor); + Envoy::Api::External::registerApi(name.c_str(), api); + } + } + +#if defined(__APPLE__) + if (respect_system_proxy_settings_) { + registerAppleProxyResolver(proxy_settings_refresh_interval_secs_); + } +#endif +} + +void MobileEngineBuilder::postRunSetup(Engine* engine) { + if (enable_network_change_monitor_) { + engine->initializeNetworkChangeMonitor(); + } +} + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/library/cc/mobile_engine_builder.h b/mobile/library/cc/mobile_engine_builder.h new file mode 100644 index 0000000000000..1f5ce8e8d69ca --- /dev/null +++ b/mobile/library/cc/mobile_engine_builder.h @@ -0,0 +1,298 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/config/core/v3/socket_option.pb.h" + +#include "source/common/protobuf/protobuf.h" + +#include "absl/container/flat_hash_map.h" +#include +#include "library/cc/engine.h" +#include "library/cc/engine_builder_base.h" +#include "library/cc/key_value_store.h" +#include "library/cc/string_accessor.h" +#include "library/common/engine_types.h" + +#include "library/cc/engine_builder_types.h" + +namespace Envoy { +namespace Platform { + +// The C++ Engine builder creates a structured bootstrap proto and modifies it through parameters +// set through the MobileEngineBuilder API calls to produce the Bootstrap config that the Engine is +// created from. +class MobileEngineBuilder : public Platform::EngineBuilderBase { +public: + MobileEngineBuilder(); + MobileEngineBuilder(MobileEngineBuilder&&) = default; + virtual ~MobileEngineBuilder() = default; + static std::string nativeNameToConfig(absl::string_view name); + + MobileEngineBuilder& addConnectTimeoutSeconds(int connect_timeout_seconds); + MobileEngineBuilder& addDnsRefreshSeconds(int dns_refresh_seconds); + MobileEngineBuilder& addDnsFailureRefreshSeconds(int base, int max); + MobileEngineBuilder& addDnsQueryTimeoutSeconds(int dns_query_timeout_seconds); + MobileEngineBuilder& setDisableDnsRefreshOnFailure(bool disable_dns_refresh_on_failure); + MobileEngineBuilder& + setDisableDnsRefreshOnNetworkChange(bool disable_dns_refresh_on_network_change); + + MobileEngineBuilder& addDnsMinRefreshSeconds(int dns_min_refresh_seconds); + MobileEngineBuilder& setDnsNumRetries(uint32_t dns_num_retries); + MobileEngineBuilder& setGetaddrinfoNumThreads(uint32_t num_threads); + MobileEngineBuilder& addMaxConnectionsPerHost(int max_connections_per_host); + MobileEngineBuilder& addH2ConnectionKeepaliveIdleIntervalMilliseconds( + int h2_connection_keepalive_idle_interval_milliseconds); + MobileEngineBuilder& + addH2ConnectionKeepaliveTimeoutSeconds(int h2_connection_keepalive_timeout_seconds); + MobileEngineBuilder& setAppVersion(std::string app_version); + MobileEngineBuilder& setAppId(std::string app_id); + MobileEngineBuilder& setDeviceOs(std::string app_id); + + MobileEngineBuilder& setPerTryIdleTimeoutSeconds(int per_try_idle_timeout_seconds); + MobileEngineBuilder& enableGzipDecompression(bool gzip_decompression_on); + MobileEngineBuilder& enableBrotliDecompression(bool brotli_decompression_on); + MobileEngineBuilder& enableSocketTagging(bool socket_tagging_on); + MobileEngineBuilder& enableHttp3(bool http3_on); + MobileEngineBuilder& enableEarlyData(bool early_data_on); + MobileEngineBuilder& enableScone(bool enable); + MobileEngineBuilder& addQuicConnectionOption(std::string option); + MobileEngineBuilder& addQuicClientConnectionOption(std::string option); + // Deprecated, use addQuicConnectionOption() instead. + MobileEngineBuilder& setHttp3ConnectionOptions(std::string options); + // Deprecated, use addQuicClientConnectionOption() instead. + MobileEngineBuilder& setHttp3ClientConnectionOptions(std::string options); + MobileEngineBuilder& addQuicHint(std::string host, int port); + MobileEngineBuilder& addQuicCanonicalSuffix(std::string suffix); + // 0 means port migration is disabled. + MobileEngineBuilder& setNumTimeoutsToTriggerPortMigration(int num_timeouts); + MobileEngineBuilder& enableInterfaceBinding(bool interface_binding_on); + MobileEngineBuilder& enableDrainPostDnsRefresh(bool drain_post_dns_refresh_on); + MobileEngineBuilder& setUdpSocketReceiveBufferSize(int32_t size); + MobileEngineBuilder& setUdpSocketSendBufferSize(int32_t size); + MobileEngineBuilder& enforceTrustChainVerification(bool trust_chain_verification_on); + MobileEngineBuilder& setUpstreamTlsSni(std::string sni); + MobileEngineBuilder& + enablePlatformCertificatesValidation(bool platform_certificates_validation_on); + // Overridden to turn off system proxying and platform certs validation while enabling worker + // thread. + MobileEngineBuilder& enableWorkerThread(bool use_worker_thread); + MobileEngineBuilder& setUseQuicPlatformPacketWriter(bool use_quic_platform_packet_writer); + // If called to enable QUIC connection migration, no need to call setUseQuicPlatformPacketWriter() + // separately. + MobileEngineBuilder& enableQuicConnectionMigration(bool quic_connection_migration_on); + MobileEngineBuilder& setMigrateIdleQuicConnection(bool migrate_idle_quic_connection); + // 0 means using the Envoy default 30s. + MobileEngineBuilder& + setMaxIdleTimeBeforeQuicMigrationSeconds(int max_idle_time_before_quic_migration); + // 0 means using the Envoy default 128s. + MobileEngineBuilder& setMaxTimeOnNonDefaultNetworkSeconds(int max_time_on_non_default_network); + + MobileEngineBuilder& enableDnsCache(bool dns_cache_on, int save_interval_seconds = 1); + // Set additional socket options on the upstream cluster outbound sockets. + MobileEngineBuilder& setAdditionalSocketOptions( + const std::vector& socket_options); + // Adds the hostnames that should be pre-resolved by DNS prior to the first request issued for + // that host. When invoked, any previous preresolve hostname entries get cleared and only the ones + // provided in the hostnames argument get set. + // TODO(abeyad): change this method and the other language APIs to take a {host,port} pair. + // E.g. addDnsPreresolveHost(std::string host, uint32_t port); + MobileEngineBuilder& addDnsPreresolveHostnames(const std::vector& hostnames); + MobileEngineBuilder& + setDnsResolver(const envoy::config::core::v3::TypedExtensionConfig& dns_resolver_config); + MobileEngineBuilder& addNativeFilter(std::string name, std::string typed_config); + MobileEngineBuilder& addNativeFilter(const std::string& name, const Protobuf::Any& typed_config); + + MobileEngineBuilder& addPlatformFilter(const std::string& name); + // Adds a runtime guard for the `envoy.reloadable_features.`. + // For example if the runtime guard is `envoy.reloadable_features.use_foo`, the guard name is + // `use_foo`. + MobileEngineBuilder& addRuntimeGuard(std::string guard, bool value); + + // These functions don't affect the Bootstrap configuration but instead perform registrations. + MobileEngineBuilder& addKeyValueStore(std::string name, KeyValueStoreSharedPtr key_value_store); + MobileEngineBuilder& addStringAccessor(std::string name, StringAccessorSharedPtr accessor); + + // Sets the thread priority of the Envoy main (network) thread. + // The value must be an integer between -20 (highest priority) and 19 (lowest priority). Values + // outside of this range will be ignored. + MobileEngineBuilder& setNetworkThreadPriority(int thread_priority); + + // Sets the QUIC connection idle timeout in seconds. + MobileEngineBuilder& + setQuicConnectionIdleTimeoutSeconds(int quic_connection_idle_timeout_seconds); + + // Sets the QUIC connection keepalive initial interval in nanoseconds + MobileEngineBuilder& setKeepAliveInitialIntervalMilliseconds(int keepalive_initial_interval_ms); + + // Sets the maximum number of concurrent streams on a multiplexed connection (HTTP/2 or HTTP/3). + MobileEngineBuilder& setMaxConcurrentStreams(int max_concurrent_streams); + + // Sets the node.id field in the Bootstrap configuration. + MobileEngineBuilder& setNodeId(std::string node_id); + // Sets the node.locality field in the Bootstrap configuration. + MobileEngineBuilder& setNodeLocality(std::string region, std::string zone, std::string sub_zone); + // Sets the node.metadata field in the Bootstrap configuration. + MobileEngineBuilder& setNodeMetadata(Protobuf::Struct node_metadata); + +#if defined(__APPLE__) + // If true, initialize the platform network change monitor to listen for network change events. + // Only takes effect on iOS, where it is required in order to enable the network change monitor. + // Defaults to false. + MobileEngineBuilder& enableNetworkChangeMonitor(bool network_change_monitor_on); +#endif + +#ifdef ENVOY_MOBILE_XDS + // Sets the xDS configuration for the Envoy Mobile engine. + // + // `xds_builder`: the XdsBuilder instance used to specify the xDS configuration options. + MobileEngineBuilder& setXds(XdsBuilder xds_builder); +#endif // ENVOY_MOBILE_XDS + +#if defined(__APPLE__) + // Right now, this API is only used by Apple (iOS) to register the Apple proxy resolver API for + // use in reading and using the system proxy settings. + // If/when we move Android system proxy registration to the C++ Engine Builder, we will make this + // API available on all platforms. + // The optional `refresh_interval_secs` parameter determines how often the system proxy settings + // are polled by the operating system; defaults to 10 seconds. If the value is <= 0, the default + // value will be used. + MobileEngineBuilder& respectSystemProxySettings(bool value, int refresh_interval_secs = 10); + MobileEngineBuilder& setIosNetworkServiceType(int ios_network_service_type); +#endif + // Overload to preserve the same return type as the EngineBuilder class. + EngineSharedPtr build(); + +private: + friend class Platform::EngineBuilderBase; + + // base class hooks + void preRunSetup(InternalEngine* engine); + void postRunSetup(Engine* engine); + absl::Status configXds(envoy::config::bootstrap::v3::Bootstrap* bootstrap); + void configureDnsCache( + envoy::extensions::common::dynamic_forward_proxy::v3::DnsCacheConfig* dns_cache_config) const; + absl::Status configureNode(envoy::config::core::v3::Node* node); + absl::Status configureRouteConfig(envoy::config::route::v3::RouteConfiguration* route_config); + absl::Status configureStaticClusters( + Protobuf::RepeatedPtrField* clusters); + absl::Status configureHttpFilters( + std::function + add_filter); + void configureCustomRouterFilter( + ::envoy::extensions::filters::http::router::v3::Router& router_config) { + (void)router_config; + } + + void addWatchdog(); + void addStatsInclusionStringMatchers(); + + struct NativeFilterConfig { + NativeFilterConfig(std::string name, std::string typed_config) + : name_(std::move(name)), textproto_typed_config_(std::move(typed_config)) {} + + NativeFilterConfig(const std::string& name, const Protobuf::Any& typed_config) + : name_(name), typed_config_(typed_config) {} + + std::string name_; + std::string textproto_typed_config_{}; + Protobuf::Any typed_config_{}; + }; + + int connect_timeout_seconds_ = 10; + int dns_refresh_seconds_ = 60; + int dns_failure_refresh_seconds_base_ = 2; + int dns_failure_refresh_seconds_max_ = 10; + int dns_query_timeout_seconds_ = 120; + bool disable_dns_refresh_on_failure_{false}; + bool disable_dns_refresh_on_network_change_{false}; + std::optional dns_num_retries_ = 3; + uint32_t getaddrinfo_num_threads_ = 1; + int h2_connection_keepalive_idle_interval_milliseconds_ = 100000000; + int h2_connection_keepalive_timeout_seconds_ = 15; + std::string app_version_ = "unspecified"; + std::string app_id_ = "unspecified"; + std::string device_os_ = "unspecified"; + int per_try_idle_timeout_seconds_ = 15; + bool gzip_decompression_filter_ = true; + bool brotli_decompression_filter_ = false; + bool socket_tagging_filter_ = false; + bool platform_certificates_validation_on_ = false; + bool dns_cache_on_ = false; + int dns_cache_save_interval_seconds_ = 1; + std::optional network_thread_priority_ = std::nullopt; + + absl::flat_hash_map key_value_stores_{}; + + bool enable_interface_binding_ = false; + bool enable_drain_post_dns_refresh_ = false; + bool enforce_trust_chain_verification_ = true; + std::string upstream_tls_sni_; + bool enable_http3_ = true; + bool enable_early_data_{true}; + bool scone_enabled_ = false; + std::string http3_connection_options_ = ""; + std::string http3_client_connection_options_ = ""; + // EVMB is to distinguish Envoy Mobile client connections. + std::vector quic_connection_options_{"AKDU", "BWRS", "5RTO", "EVMB"}; + std::vector quic_client_connection_options_; + std::vector> quic_hints_; + std::vector quic_suffixes_; + int num_timeouts_to_trigger_port_migration_ = 0; +#if defined(__APPLE__) + bool respect_system_proxy_settings_ = true; + int proxy_settings_refresh_interval_secs_ = 10; + int ios_network_service_type_ = 0; +#endif + int dns_min_refresh_seconds_ = 60; + int max_connections_per_host_ = 7; + + std::vector native_filter_chain_; + std::vector> dns_preresolve_hostnames_; + std::optional dns_resolver_config_; + std::vector socket_options_; + + absl::flat_hash_map string_accessors_; + + // This is the same value Cronet uses for QUIC: + // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_context.h;drc=ccfe61524368c94b138ddf96ae8121d7eb7096cf;l=87 + int32_t udp_socket_receive_buffer_size_ = 1024 * 1024; // 1MB + // This is the same value Cronet uses for QUIC: + // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_session_pool.cc;l=790-793;drc=7f04a8e033c23dede6beae129cd212e6d4473d72 + // https://source.chromium.org/chromium/chromium/src/+/main:net/third_party/quiche/src/quiche/quic/core/quic_constants.h;l=43-47;drc=34ad7f3844f882baf3d31a6bc6e300acaa0e3fc8 + int32_t udp_socket_send_buffer_size_ = 1452 * 20; + // These are the same values Cronet uses for QUIC: + // https://source.chromium.org/chromium/chromium/src/+/main:net/quic/quic_context.cc;l=21-22;drc=6849bf6b37e96bd1c38a5f77f7deaa28b53779c4;bpv=1;bpt=1 + const uint32_t initial_stream_window_size_ = 6 * 1024 * 1024; // 6MB + const uint32_t initial_connection_window_size_ = 15 * 1024 * 1024; // 15MB + int quic_connection_idle_timeout_seconds_ = 60; + + int keepalive_initial_interval_ms_ = 0; + int max_concurrent_streams_ = 0; + bool use_quic_platform_packet_writer_ = false; + + // QUIC connection migration. + bool enable_quic_connection_migration_ = false; + bool migrate_idle_quic_connection_ = false; + int max_idle_time_before_quic_migration_seconds_ = 0; + int max_time_on_non_default_network_seconds_ = 0; + + std::string node_id_; + std::optional node_locality_ = std::nullopt; + std::optional node_metadata_ = std::nullopt; + + bool enable_network_change_monitor_ = false; +#ifdef ENVOY_MOBILE_XDS + std::optional xds_builder_ = std::nullopt; +#endif // ENVOY_MOBILE_XDS +}; + +using MobileEngineBuilderSharedPtr = std::shared_ptr; + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/library/cc/stream_client.cc b/mobile/library/cc/stream_client.cc index f780ca454044f..615f7639602c7 100644 --- a/mobile/library/cc/stream_client.cc +++ b/mobile/library/cc/stream_client.cc @@ -1,12 +1,18 @@ #include "library/cc/stream_client.h" +#include +#include + +#include "absl/strings/string_view.h" + namespace Envoy { namespace Platform { -StreamClient::StreamClient(EngineSharedPtr engine) : engine_(engine) {} +StreamClient::StreamClient(EngineSharedPtr engine, absl::string_view listener_name) + : engine_(engine), listener_name_(std::string(listener_name)) {} StreamPrototypeSharedPtr StreamClient::newStreamPrototype() { - return std::make_shared(engine_); + return std::make_shared(engine_, listener_name_); } } // namespace Platform diff --git a/mobile/library/cc/stream_client.h b/mobile/library/cc/stream_client.h index 0bac6267a2d5a..1afe10efe2998 100644 --- a/mobile/library/cc/stream_client.h +++ b/mobile/library/cc/stream_client.h @@ -1,7 +1,9 @@ #pragma once #include +#include +#include "absl/strings/string_view.h" #include "library/cc/engine.h" #include "library/cc/stream_prototype.h" @@ -16,12 +18,17 @@ using StreamPrototypeSharedPtr = std::shared_ptr; class StreamClient { public: - StreamClient(EngineSharedPtr engine); + /** + * @param engine The underlying engine. + * @param listener_name The name of the listener this client will route streams to. + */ + StreamClient(EngineSharedPtr engine, absl::string_view listener_name); StreamPrototypeSharedPtr newStreamPrototype(); private: EngineSharedPtr engine_; + std::string listener_name_; }; using StreamClientSharedPtr = std::shared_ptr; diff --git a/mobile/library/cc/stream_prototype.cc b/mobile/library/cc/stream_prototype.cc index 4ac6e8357ef6f..2512ed54bb265 100644 --- a/mobile/library/cc/stream_prototype.cc +++ b/mobile/library/cc/stream_prototype.cc @@ -1,17 +1,25 @@ #include "stream_prototype.h" +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "library/cc/stream.h" #include "library/common/internal_engine.h" namespace Envoy { namespace Platform { -StreamPrototype::StreamPrototype(EngineSharedPtr engine) : engine_(engine) {} +StreamPrototype::StreamPrototype(EngineSharedPtr engine, absl::string_view listener_name) + : engine_(engine), listener_name_(std::string(listener_name)) {} StreamSharedPtr StreamPrototype::start(EnvoyStreamCallbacks&& stream_callbacks, bool explicit_flow_control) { - auto envoy_stream = engine_->engine_->initStream(); - engine_->engine_->startStream(envoy_stream, std::move(stream_callbacks), explicit_flow_control); - return std::make_shared(engine_->engine_, envoy_stream); + auto envoy_stream = engine_->engine()->initStream(); + engine_->engine()->startStream(envoy_stream, std::move(stream_callbacks), explicit_flow_control, + listener_name_); + return std::make_shared(engine_->engine(), envoy_stream); } } // namespace Platform diff --git a/mobile/library/cc/stream_prototype.h b/mobile/library/cc/stream_prototype.h index fca1eb8d91e25..1aaef614c29d6 100644 --- a/mobile/library/cc/stream_prototype.h +++ b/mobile/library/cc/stream_prototype.h @@ -1,7 +1,9 @@ #pragma once #include +#include +#include "absl/strings/string_view.h" #include "library/cc/engine.h" #include "library/cc/stream.h" #include "library/common/engine_types.h" @@ -14,7 +16,11 @@ using EngineSharedPtr = std::shared_ptr; class StreamPrototype { public: - explicit StreamPrototype(EngineSharedPtr engine); + /** + * @param engine The underlying engine. + * @param listener_name The name of the listener this stream will be started on. + */ + StreamPrototype(EngineSharedPtr engine, absl::string_view listener_name); /** Starts the stream. */ StreamSharedPtr start(EnvoyStreamCallbacks&& stream_callbacks, @@ -22,6 +28,7 @@ class StreamPrototype { private: EngineSharedPtr engine_; + std::string listener_name_; }; using StreamPrototypeSharedPtr = std::shared_ptr; diff --git a/mobile/library/cc/string_accessor.h b/mobile/library/cc/string_accessor.h index 40bf0c35abdb7..9bc35fca83aec 100644 --- a/mobile/library/cc/string_accessor.h +++ b/mobile/library/cc/string_accessor.h @@ -4,7 +4,7 @@ #include "envoy/common/pure.h" -#include "absl/types/optional.h" +#include #include "library/common/api/c_types.h" namespace Envoy { diff --git a/mobile/library/common/BUILD b/mobile/library/common/BUILD index 54e0243d760d5..ce6bb011ddc60 100644 --- a/mobile/library/common/BUILD +++ b/mobile/library/common/BUILD @@ -28,6 +28,7 @@ envoy_cc_library( ":mobile_process_wide_lib", "//library/common/bridge:utility_lib", "//library/common/event:provisional_dispatcher_lib", + "//library/common/extensions/listener_managers/api_listener_manager:api_listener_manager_lib", "//library/common/http:client_lib", "//library/common/http:header_utility_lib", "//library/common/logger:logger_delegate_lib", diff --git a/mobile/library/common/api/external.cc b/mobile/library/common/api/external.cc index e62ceacaa578d..718087a3b5e92 100644 --- a/mobile/library/common/api/external.cc +++ b/mobile/library/common/api/external.cc @@ -32,6 +32,8 @@ void* retrieveApi(absl::string_view name, bool allow_absent) { return api; } +void unregisterApi(absl::string_view name) { registry_.erase(name); } + } // namespace External } // namespace Api } // namespace Envoy diff --git a/mobile/library/common/api/external.h b/mobile/library/common/api/external.h index 1761995280dc0..f8f4496dcc451 100644 --- a/mobile/library/common/api/external.h +++ b/mobile/library/common/api/external.h @@ -18,6 +18,11 @@ void registerApi(std::string&& name, void* api); */ void* retrieveApi(absl::string_view name, bool allow_absent = false); +/** + * Unregister an external runtime API. + */ +void unregisterApi(absl::string_view name); + } // namespace External } // namespace Api } // namespace Envoy diff --git a/mobile/library/common/engine_common.cc b/mobile/library/common/engine_common.cc index 03f0e1719e819..d3ee363d8debd 100644 --- a/mobile/library/common/engine_common.cc +++ b/mobile/library/common/engine_common.cc @@ -81,7 +81,9 @@ class ServerLite : public Server::InstanceBase { } }; -EngineCommon::EngineCommon(std::shared_ptr options) : options_(options) { +EngineCommon::EngineCommon(std::shared_ptr options, + std::function on_workers_started) + : options_(options), mobile_listener_hooks_(on_workers_started) { #if !defined(ENVOY_ENABLE_FULL_PROTOS) registerMobileProtoDescriptors(); @@ -106,7 +108,7 @@ EngineCommon::EngineCommon(std::shared_ptr options) : op auto random_generator = std::make_unique(); base_ = std::make_unique(*options_, prod_component_factory_, std::make_unique(), *random_generator); - base_->init(real_time_system_, default_listener_hooks_, std::move(random_generator), nullptr, + base_->init(real_time_system_, mobile_listener_hooks_, std::move(random_generator), nullptr, create_instance); // Disabling signal handling in the options makes it so that the server's event dispatcher _does // not_ listen for termination signals such as SIGTERM, SIGINT, etc diff --git a/mobile/library/common/engine_common.h b/mobile/library/common/engine_common.h index 0b860b6173178..147de047e2880 100644 --- a/mobile/library/common/engine_common.h +++ b/mobile/library/common/engine_common.h @@ -14,19 +14,38 @@ #include "source/exe/terminate_handler.h" #endif +#include + namespace Envoy { // If Envoy is built with lite protos, this will register Envoy-Mobile specific // descriptors for reflection. void registerMobileProtoDescriptors(); +class MobileListenerHooks : public ListenerHooks { +public: + MobileListenerHooks(std::function on_workers_started) + : on_workers_started_(on_workers_started) {} + void onWorkerListenerAdded() override {} + void onWorkerListenerRemoved() override {} + void onWorkersStarted() override { + if (on_workers_started_) { + on_workers_started_(); + } + } + +private: + std::function on_workers_started_; +}; + /** * This class is used instead of Envoy::MainCommon to customize logic for the Envoy Mobile setting. * It largely leverages Envoy::StrippedMainBase. */ class EngineCommon { public: - EngineCommon(std::shared_ptr options); + EngineCommon(std::shared_ptr options, + std::function on_workers_started = nullptr); bool run() { base_->runServer(); return true; @@ -47,7 +66,7 @@ class EngineCommon { #endif std::shared_ptr options_; Event::RealTimeSystem real_time_system_; // NO_CHECK_FORMAT(real_time) - DefaultListenerHooks default_listener_hooks_; + MobileListenerHooks mobile_listener_hooks_; ProdComponentFactory prod_component_factory_; std::shared_ptr base_; }; diff --git a/mobile/library/common/engine_types.h b/mobile/library/common/engine_types.h index 082a6789a8573..72714b2012237 100644 --- a/mobile/library/common/engine_types.h +++ b/mobile/library/common/engine_types.h @@ -28,6 +28,7 @@ struct EnvoyLogger { }; inline constexpr absl::string_view ENVOY_EVENT_TRACKER_API_NAME = "event_tracker_api"; +inline constexpr absl::string_view DEFAULT_API_LISTENER_NAME = "base_api_listener"; /** The callbacks for Envoy Event Tracker. */ struct EnvoyEventTracker { @@ -40,7 +41,7 @@ struct EnvoyEventTracker { struct EnvoyError { envoy_error_code_t error_code_; std::string message_; - absl::optional attempt_count_ = absl::nullopt; + std::optional attempt_count_ = std::nullopt; }; /** The callbacks for the stream. */ diff --git a/mobile/library/common/event/BUILD b/mobile/library/common/event/BUILD index a04e39c312ed9..0f52a1b759407 100644 --- a/mobile/library/common/event/BUILD +++ b/mobile/library/common/event/BUILD @@ -11,7 +11,6 @@ envoy_cc_library( repository = "@envoy", deps = [ "//library/common/types:c_types_lib", - "@abseil-cpp//absl/types:optional", "@envoy//envoy/event:deferred_deletable", "@envoy//envoy/event:dispatcher_interface", "@envoy//source/common/common:lock_guard_lib", diff --git a/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc b/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc index e3151f4a3564c..85bccf4aa206b 100644 --- a/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc +++ b/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc @@ -68,13 +68,13 @@ ValidationResults PlatformBridgeCertValidator::doVerifyCertChain( stats_.fail_verify_error_.inc(); ENVOY_LOG(debug, error); return {ValidationResults::ValidationStatus::Failed, - Envoy::Ssl::ClientValidationStatus::NotValidated, absl::nullopt, error}; + Envoy::Ssl::ClientValidationStatus::NotValidated, std::nullopt, error}; } if (callback == nullptr) { IS_ENVOY_BUG("No callback specified"); const char* error = "verify cert chain failed: no callback specified."; return {ValidationResults::ValidationStatus::Failed, - Envoy::Ssl::ClientValidationStatus::NotValidated, absl::nullopt, error}; + Envoy::Ssl::ClientValidationStatus::NotValidated, std::nullopt, error}; } std::vector certs; @@ -109,6 +109,8 @@ ValidationResults PlatformBridgeCertValidator::doVerifyCertChain( ValidationJob job; job.result_callback_ = std::move(callback); Event::Dispatcher& dispatcher = job.result_callback_->dispatcher(); + ASSERT(Thread::MainThread::isMainOrTestThread(), + "Certification validation must happen on the main thread."); Thread::Options thread_options; thread_options.priority_ = thread_priority_; job.validation_thread_ = thread_factory_->createThread( @@ -119,13 +121,13 @@ ValidationResults PlatformBridgeCertValidator::doVerifyCertChain( thread_options, /* crash_on_failure=*/false); if (job.validation_thread_ == nullptr) { return {ValidationResults::ValidationStatus::Failed, - Envoy::Ssl::ClientValidationStatus::NotValidated, absl::nullopt, + Envoy::Ssl::ClientValidationStatus::NotValidated, std::nullopt, "Failed creating a thread for cert chain validation."}; } Thread::ThreadId thread_id = job.validation_thread_->pthreadId(); validation_jobs_[thread_id] = std::move(job); return {ValidationResults::ValidationStatus::Pending, - Envoy::Ssl::ClientValidationStatus::NotValidated, absl::nullopt, absl::nullopt}; + Envoy::Ssl::ClientValidationStatus::NotValidated, std::nullopt, std::nullopt}; } void PlatformBridgeCertValidator::verifyCertChainByPlatform( diff --git a/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h b/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h index a2c22b0644e35..674ddf7a07128 100644 --- a/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h +++ b/mobile/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h @@ -40,7 +40,7 @@ class PlatformBridgeCertValidator : public CertValidator, Logger::Loggable daysUntilFirstCertExpires() const override { return absl::nullopt; } + std::optional daysUntilFirstCertExpires() const override { return std::nullopt; } Envoy::Ssl::CertificateDetailsPtr getCaCertInformation() const override { return nullptr; } // Return empty string std::string getCaFileName() const override { return ""; } @@ -107,7 +107,7 @@ class PlatformBridgeCertValidator : public CertValidator, Logger::Loggable validation_jobs_; std::shared_ptr alive_indicator_{new size_t(1)}; Thread::PosixThreadFactoryPtr thread_factory_; - absl::optional thread_priority_; + std::optional thread_priority_; }; } // namespace Tls diff --git a/mobile/library/common/extensions/filters/http/network_configuration/filter.cc b/mobile/library/common/extensions/filters/http/network_configuration/filter.cc index b5c003750e0b1..c3041a8a1e298 100644 --- a/mobile/library/common/extensions/filters/http/network_configuration/filter.cc +++ b/mobile/library/common/extensions/filters/http/network_configuration/filter.cc @@ -30,7 +30,7 @@ void NetworkConfigurationFilter::setDecoderFilterCallbacks( decoder_callbacks_ = &callbacks; decoder_callbacks_->streamInfo().filterState()->setData( StreamInfo::ExtraStreamInfo::key(), std::move(new_extra_stream_info), - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Request); + StreamInfo::FilterState::LifeSpan::Request); auto options = std::make_shared(); connectivity_manager_->setInterfaceBindingEnabled(enable_interface_binding_); @@ -58,7 +58,7 @@ bool NetworkConfigurationFilter::onAddressResolved( } decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Proxy configured but DNS resolution failed", nullptr, - absl::nullopt, "no_dns_address_for_proxy"); + std::nullopt, "no_dns_address_for_proxy"); return false; } @@ -103,7 +103,7 @@ NetworkConfigurationFilter::resolveProxy(Http::RequestHeaderMap& request_headers "NetworkConfigurationProxy::resolveProxy not running on main thread."); ASSERT(proxy_resolver != nullptr, "proxy_resolver must not be null."); - const std::string target_url = Http::Utility::buildOriginalUri(request_headers, absl::nullopt); + const std::string target_url = Http::Utility::buildOriginalUri(request_headers, std::nullopt); std::weak_ptr weak_self = weak_from_this(); Network::ProxyResolutionResult proxy_resolution_result = proxy_resolver->resolver->resolveProxy( @@ -158,7 +158,7 @@ Http::FilterHeadersStatus NetworkConfigurationFilter::continueWithProxySettings( if (!connectivity_manager_->dnsCache()) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Proxy configured but no DNS cache available", nullptr, - absl::nullopt, "no_dns_cache_for_proxy"); + std::nullopt, "no_dns_cache_for_proxy"); return Http::FilterHeadersStatus::StopIteration; } @@ -186,7 +186,7 @@ Http::FilterHeadersStatus NetworkConfigurationFilter::continueWithProxySettings( // If DNS lookup straight up fails, fail the request. decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Proxy configured but DNS resolution failed", nullptr, - absl::nullopt, "no_dns_address_for_proxy"); + std::nullopt, "no_dns_address_for_proxy"); return Http::FilterHeadersStatus::StopIteration; } @@ -196,7 +196,7 @@ void NetworkConfigurationFilter::setInfo(absl::string_view authority, decoder_callbacks_->streamInfo().filterState()->setData( Network::Http11ProxyInfoFilterState::key(), std::make_unique(authority, address), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); + StreamInfo::FilterState::LifeSpan::FilterChain); } Http::FilterHeadersStatus NetworkConfigurationFilter::encodeHeaders(Http::ResponseHeaderMap&, diff --git a/mobile/library/common/extensions/filters/http/socket_tag/filter.cc b/mobile/library/common/extensions/filters/http/socket_tag/filter.cc index abe4006cc1267..bdf51479a1ec2 100644 --- a/mobile/library/common/extensions/filters/http/socket_tag/filter.cc +++ b/mobile/library/common/extensions/filters/http/socket_tag/filter.cc @@ -28,7 +28,7 @@ Http::FilterHeadersStatus SocketTagFilter::decodeHeaders(Http::RequestHeaderMap& decoder_callbacks_->sendLocalReply( Http::Code::BadRequest, absl::StrCat("Invalid x-envoy-mobile-socket-tag header: ", tag_string), nullptr, - absl::nullopt, ""); + std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } auto options = std::make_shared(); diff --git a/mobile/library/common/extensions/listener_managers/api_listener_manager/BUILD b/mobile/library/common/extensions/listener_managers/api_listener_manager/BUILD index a6027931e1aad..3e0e1068571cf 100644 --- a/mobile/library/common/extensions/listener_managers/api_listener_manager/BUILD +++ b/mobile/library/common/extensions/listener_managers/api_listener_manager/BUILD @@ -18,11 +18,16 @@ envoy_cc_extension( ], repository = "@envoy", deps = [ + "//library/common:engine_types_lib", + "//library/common/event:provisional_dispatcher_lib", + "//library/common/http:client_lib", "@envoy//envoy/server:api_listener_interface", "@envoy//envoy/server:instance_interface", "@envoy//envoy/server:listener_manager_interface", + "@envoy//envoy/thread:thread_interface", "@envoy//source/extensions/api_listeners/default_api_listener:api_listener_lib", "@envoy//source/server:listener_manager_factory_lib", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", ], ) diff --git a/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.cc b/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.cc index c851f89d4aabe..1df5cc51fe22c 100644 --- a/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.cc +++ b/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.cc @@ -1,9 +1,21 @@ #include "library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h" +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include +#include "envoy/server/listener_manager.h" +#include "source/common/common/logger.h" + +#include "absl/synchronization/notification.h" #include "envoy/config/listener/v3/listener.pb.h" #include "envoy/network/listener.h" #include "envoy/server/api_listener.h" - +#include "library/common/engine_types.h" #include "source/common/common/assert.h" #include "source/common/common/fmt.h" #include "source/common/config/utility.h" @@ -11,7 +23,147 @@ namespace Envoy { namespace Server { -ApiListenerManagerImpl::ApiListenerManagerImpl(Instance& server) : server_(server) {} +ApiListenerWorker::ApiListenerWorker(Instance& server) + : server_(server), dispatcher_(server.api().allocateDispatcher("api_listener_worker")), + provisional_dispatcher_(std::make_unique()) { + server_.threadLocal().registerThread(*dispatcher_, false); + provisional_dispatcher_->drain(*dispatcher_); +} + +ApiListenerWorker::~ApiListenerWorker() {} + +void ApiListenerWorker::addListener(const std::string& name, + std::shared_ptr api_listener, + std::function cb) { + dispatcher_->post([this, name, api_listener, cb]() { + if (api_listener != nullptr) { + auto it = http_clients_.find(name); + if (it != http_clients_.end()) { + it->second->shutdownApiListener(); + http_clients_.erase(it); + } + auto api_listener_impl = api_listener->createHttpApiListener(*dispatcher_); + ASSERT(api_listener_impl != nullptr); + http_clients_[name] = std::make_unique( + std::move(api_listener_impl), *provisional_dispatcher_, + server_.serverFactoryContext().scope(), server_.api().randomGenerator()); + ENVOY_LOG_MISC(info, "Created API listener {}.", name); + if (shutdown_notification_.HasBeenNotified()) { + ENVOY_LOG_MISC(info, "Shutting down API listener {}.", name); + http_clients_[name]->shutdownApiListener(); + } + } + if (cb) { + server_.dispatcher().post([cb]() { cb(); }); + } + }); +} + +void ApiListenerWorker::start(OptRef guard_dog, const std::function& cb) { + ASSERT(!thread_); + + Thread::Options options{absl::StrCat("wrk:", dispatcher_->name())}; + thread_ = server_.api().threadFactory().createThread( + [this, guard_dog, cb]() -> void { threadRoutine(guard_dog, cb); }, options); +} + +void ApiListenerWorker::initializeStats(Stats::Scope& scope) { + if (dispatcher_) { + dispatcher_->initializeStats(scope); + } +} + +void ApiListenerWorker::stop() { + if (thread_) { + if (!shutdown_notification_.HasBeenNotified()) { + stopListener(); + } + shutdown_notification_.WaitForNotification(); + ENVOY_LOG_MISC(info, "Listener has been shutdown, exiting the event loop."); + dispatcher_->exit(); + thread_->join(); + thread_.reset(); + } +} + +void ApiListenerWorker::stopListener() { + if (stop_posted_.exchange(true)) { + return; + } + dispatcher_->post([this]() { + for (auto& [name, client] : http_clients_) { + if (client) { + client->shutdownApiListener(); + } + } + shutdown_notification_.Notify(); + }); +} + +void ApiListenerWorker::threadRoutine(OptRef guard_dog, const std::function& cb) { + if (cb) { + dispatcher_->post(cb); + } + if (guard_dog.has_value()) { + dispatcher_->post([this, guard_dog]() { + watch_dog_ = guard_dog->createWatchDog(server_.api().threadFactory().currentThreadId(), + dispatcher_->name(), *dispatcher_); + }); + } + + dispatcher_->run(Event::Dispatcher::RunType::RunUntilExit); + dispatcher_->shutdown(); + ENVOY_LOG_MISC(info, "ApiListenerWorker dispatcher exited"); + if (guard_dog.has_value() && watch_dog_) { + guard_dog->stopWatching(watch_dog_); + } + + watch_dog_.reset(); +} + +ApiListenerManagerImpl::ApiListenerManagerImpl(Instance& server, bool use_worker_thread) + : server_(server), + worker_(use_worker_thread ? std::make_unique(server) : nullptr) {} + +ApiListenerManagerImpl::~ApiListenerManagerImpl() { + if (worker_) { + stopWorkers(); + } +} + +ApiListenerOptRef ApiListenerManagerImpl::apiListener() { + auto it = api_listeners_.find(DEFAULT_API_LISTENER_NAME); + if (it != api_listeners_.end()) { + return ApiListenerOptRef(std::ref(*it->second)); + } + if (api_listeners_.empty()) { + return std::nullopt; + } + // Fall back to the first listener if the default one is not found but we have + // one. + return ApiListenerOptRef(std::ref(*api_listeners_.begin()->second)); +} + +ApiListenerOptRef ApiListenerManagerImpl::apiListener(absl::string_view name) { + auto it = api_listeners_.find(name); + if (it != api_listeners_.end()) { + return ApiListenerOptRef(std::ref(*it->second)); + } + return std::nullopt; +} + +Http::Client* ApiListenerManagerImpl::httpClient(const std::string& name) { + if (worker_) { + return worker_->httpClient(name); + } + ASSERT(api_listeners_.size() <= 1); + if (!name.empty() && !api_listeners_.empty()) { + if (name != api_listeners_.begin()->first) { + return nullptr; + } + } + return http_client_.get(); +} absl::StatusOr ApiListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::Listener& config, @@ -21,21 +173,16 @@ ApiListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::L if (!config.name().empty()) { name = config.name(); } else { - // TODO (soulxu): The random uuid name is bad for logging. We can use listening addresses in - // the log to improve that. name = server_.api().randomGenerator().uuid(); } - // TODO(junr03): currently only one ApiListener can be installed via bootstrap to avoid having to - // build a collection of listeners, and to have to be able to warm and drain the listeners. In the - // future allow multiple ApiListeners, and allow them to be created via LDS as well as bootstrap. if (config.has_api_listener()) { if (config.has_internal_listener()) { return absl::InvalidArgumentError(fmt::format( "error adding listener named '{}': api_listener and internal_listener cannot be both set", name)); } - if (!api_listener_ && !added_via_api) { + if (!added_via_api) { auto* api_listener_factory = Registry::FactoryRegistry::getFactory( "envoy.http_api_listener"); @@ -43,19 +190,127 @@ ApiListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::L return absl::InvalidArgumentError(fmt::format( "error adding listener named '{}': missing the API listener extension", name)); } - auto listener_or_error = api_listener_factory->create(config, server_, config.name()); + auto listener_or_error = api_listener_factory->create(config, server_, name); RETURN_IF_NOT_OK(listener_or_error.status()); - api_listener_ = std::move(listener_or_error.value()); + + // TODO(junr03): support warming and draining of API listeners when + // multiple are installed. + bool is_update = (api_listeners_.find(name) != api_listeners_.end()); + + // If there is no worker thread, we only support a single API listener. + // We check if this is an update to the existing one, or if we are trying + // to add a new one when we already have one. + if (!worker_ && !is_update && !api_listeners_.empty()) { + return absl::InvalidArgumentError( + "Multiple ApiListeners are not supported when worker thread is " + "disabled."); + } + + if (is_update && worker_started_ && !worker_) { + if (http_client_) { + http_client_->shutdownApiListener(); + http_client_.reset(); + } + } + + api_listeners_[name] = std::move(listener_or_error.value()); + + if (worker_started_) { + if (!worker_) { + setupClient(); + } else { + worker_->addListener(name, api_listeners_[name], nullptr); + } + } return true; } else { - ENVOY_LOG(warn, "listener {} can not be added because currently only one ApiListener is " - "allowed, and it can only be added via bootstrap configuration"); + ENVOY_LOG(warn, + "listener {} can not be added because it can only be added via " + "bootstrap configuration", + name); return false; } } return false; } +absl::Status ApiListenerManagerImpl::startWorkers(OptRef guard_dog, + std::function callback) { + if (worker_started_.exchange(true)) { + if (callback) { + callback(); + } + return absl::OkStatus(); + } + + if (!worker_) { + setupClient(); + if (provisional_dispatcher_) { + provisional_dispatcher_->drain(server_.dispatcher()); + } + if (callback) { + callback(); + } + return absl::OkStatus(); + } + + if (api_listeners_.empty()) { + if (callback) { + callback(); + } + return absl::OkStatus(); + } + + size_t remaining = api_listeners_.size(); + size_t i = 0; + for (auto& [name, listener] : api_listeners_) { + bool is_last = (i == remaining - 1); + worker_->addListener(name, listener, is_last ? callback : nullptr); + i++; + } + + absl::Notification worker_started_notification; + worker_->start(guard_dog, + [&worker_started_notification]() { worker_started_notification.Notify(); }); + worker_started_notification.WaitForNotification(); + ENVOY_LOG_MISC(info, "Worker thread has been started"); + + return absl::OkStatus(); +} + +void ApiListenerManagerImpl::stopListeners(StopListenersType, + const Network::ExtraShutdownListenerOptions&) { + if (worker_) { + worker_->stopListener(); + } else { + if (http_client_) { + http_client_->shutdownApiListener(); + } + } +} + +void ApiListenerManagerImpl::stopWorkers() { + if (worker_started_.load() && worker_) { + worker_->stop(); + } +} + +void ApiListenerManagerImpl::setupClient() { + ASSERT(!worker_); + ASSERT(api_listeners_.size() <= 1); + if (api_listeners_.empty()) { + return; + } + auto& name = api_listeners_.begin()->first; + if (!provisional_dispatcher_) { + provisional_dispatcher_ = std::make_unique(); + } + auto api_listener_impl = api_listeners_[name]->createHttpApiListener(server_.dispatcher()); + http_client_ = std::make_unique( + std::move(api_listener_impl), *provisional_dispatcher_, + server_.serverFactoryContext().scope(), server_.api().randomGenerator()); +} + REGISTER_FACTORY(ApiListenerManagerFactoryImpl, ListenerManagerFactory); } // namespace Server diff --git a/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h b/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h index ddec0dd782133..82c7cc8c56389 100644 --- a/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h +++ b/mobile/library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h @@ -2,21 +2,71 @@ #include +#include "absl/container/flat_hash_map.h" +#include "absl/synchronization/notification.h" +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" +#include "envoy/config/bootstrap/v3/bootstrap.pb.validate.h" #include "envoy/server/instance.h" #include "envoy/server/listener_manager.h" - +#include "envoy/thread/thread.h" +#include "library/common/event/provisional_dispatcher.h" +#include "library/common/http/client.h" +#include "library/common/engine_types.h" #include "source/server/listener_manager_factory.h" namespace Envoy { namespace Server { +class ApiListenerWorker { +public: + ApiListenerWorker(Instance& server); + ~ApiListenerWorker(); + + void addListener(const std::string& name, std::shared_ptr api_listener, + std::function cb); + void start(OptRef guard_dog, const std::function& cb); + void initializeStats(Stats::Scope& scope); + void stop(); + void stopListener(); + + Http::Client* httpClient(const std::string& name = "") { + std::string target_name = name; + if (target_name.empty()) { + if (http_clients_.size() == 1) { + return http_clients_.begin()->second.get(); + } + target_name = std::string(DEFAULT_API_LISTENER_NAME); + } + auto it = http_clients_.find(target_name); + if (it != http_clients_.end()) { + return it->second.get(); + } + return nullptr; + } + Event::Dispatcher& dispatcher() { return *dispatcher_; } + +private: + void threadRoutine(OptRef guard_dog, const std::function& cb); + + Instance& server_; + Thread::ThreadPtr thread_; + Event::DispatcherPtr dispatcher_; + // Wraps above dispatcher_ for the http client. + std::unique_ptr provisional_dispatcher_; + absl::flat_hash_map http_clients_; + WatchDogSharedPtr watch_dog_; + absl::Notification shutdown_notification_; + std::atomic stop_posted_{false}; +}; + /** * Implementation of a lightweight ListenerManager for Envoy Mobile. * This does not handle downstream TCP / UDP connections but only the API listener. */ class ApiListenerManagerImpl : public ListenerManager, Logger::Loggable { public: - explicit ApiListenerManagerImpl(Instance& server); + explicit ApiListenerManagerImpl(Instance& server, bool use_worker_thread = false); + ~ApiListenerManagerImpl() override; // Server::ListenerManager absl::StatusOr addOrUpdateListener(const envoy::config::listener::v3::Listener& config, @@ -29,40 +79,56 @@ class ApiListenerManagerImpl : public ListenerManager, Logger::Loggable, std::function callback) override { - callback(); - return absl::OkStatus(); - } - void stopListeners(StopListenersType, const Network::ExtraShutdownListenerOptions&) override {} - void stopWorkers() override {} + absl::Status startWorkers(OptRef guard_dog, std::function callback) override; + void stopListeners(StopListenersType, const Network::ExtraShutdownListenerOptions&) override; + void stopWorkers() override; void beginListenerUpdate() override {} void endListenerUpdate(FailureStates&&) override {} - bool isWorkerStarted() override { return true; } + bool isWorkerStarted() override { return worker_started_.load(); } Http::Context& httpContext() { return server_.httpContext(); } - ApiListenerOptRef apiListener() override { - return api_listener_ ? ApiListenerOptRef(std::ref(*api_listener_)) : absl::nullopt; - } + ApiListenerOptRef apiListener() override; + ApiListenerOptRef apiListener(absl::string_view name) override; ListenerUpdateCallbacksHandlePtr addListenerUpdateCallbacks(ListenerUpdateCallbacks&) override { return std::make_unique(); } + Http::Client* httpClient(const std::string& name = ""); + + Event::Dispatcher& httpClientDispatcher() { + ASSERT(worker_ != nullptr, "httpClientDispatcher() called when worker_ is null!"); + return worker_->dispatcher(); + } + private: struct ListenerUpdateCallbacksNopHandle : public ListenerUpdateCallbacksHandle {}; Instance& server_; - ApiListenerPtr api_listener_; + absl::flat_hash_map> api_listeners_; + + std::atomic worker_started_{false}; + std::unique_ptr provisional_dispatcher_; + Http::ClientPtr http_client_; + std::unique_ptr worker_; + + void setupClient(); }; class ApiListenerManagerFactoryImpl : public ListenerManagerFactory { public: std::unique_ptr - createListenerManager(const Protobuf::Message&, Instance& server, + createListenerManager(const Protobuf::Message& config, Instance& server, std::unique_ptr&&, WorkerFactory&, bool, Quic::QuicStatNames&) override { - return std::make_unique(server); + const auto& api_config = + MessageUtil::downcastAndValidate( + config, server.messageValidationContext().staticValidationVisitor()); + bool use_worker_thread = + (api_config.threading_model() == + envoy::config::bootstrap::v3::ApiListenerManager::STANDALONE_WORKER_THREAD); + return std::make_unique(server, use_worker_thread); } std::string name() const override { return "envoy.listener_manager_impl.api"; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; diff --git a/mobile/library/common/extensions/retry/options/network_configuration/predicate.cc b/mobile/library/common/extensions/retry/options/network_configuration/predicate.cc index 7f6816728c481..456ef2fb13102 100644 --- a/mobile/library/common/extensions/retry/options/network_configuration/predicate.cc +++ b/mobile/library/common/extensions/retry/options/network_configuration/predicate.cc @@ -30,7 +30,7 @@ NetworkConfigurationRetryOptionsPredicate::updateOptions( ENVOY_LOG(warn, "extra stream info is missing"); // Returning nullopt results in existing socket options being preserved. - return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{absl::nullopt}; + return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{std::nullopt}; } StreamInfo::ExtraStreamInfo* extra_stream_info = @@ -40,7 +40,7 @@ NetworkConfigurationRetryOptionsPredicate::updateOptions( ENVOY_LOG(warn, "extra stream info is missing"); // Returning nullopt results in existing socket options being preserved. - return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{absl::nullopt}; + return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{std::nullopt}; } // This check is also defensive. The NetworkConfigurationFilter should always set this when @@ -49,7 +49,7 @@ NetworkConfigurationRetryOptionsPredicate::updateOptions( ENVOY_LOG(warn, "network configuration key is missing"); // Returning nullopt results in existing socket options being preserved. - return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{absl::nullopt}; + return Upstream::RetryOptionsPredicate::UpdateOptionsReturn{std::nullopt}; } // As a proxy for the many different types of network errors, this code interprets any failure diff --git a/mobile/library/common/http/BUILD b/mobile/library/common/http/BUILD index a06c83a836e90..eda9351cd6b2f 100644 --- a/mobile/library/common/http/BUILD +++ b/mobile/library/common/http/BUILD @@ -22,7 +22,6 @@ envoy_cc_library( "//library/common/stream_info:extra_stream_info_lib", "//library/common/system:system_helper_lib", "//library/common/types:c_types_lib", - "@abseil-cpp//absl/types:optional", "@envoy//envoy/buffer:buffer_interface", "@envoy//envoy/common:scope_tracker_interface", "@envoy//envoy/event:deferred_deletable", @@ -42,6 +41,7 @@ envoy_cc_library( "@envoy//source/common/http:headers_lib", "@envoy//source/common/http:utility_lib", "@envoy//source/common/network:socket_lib", + "@envoy//source/common/quic:scone_state", "@envoy//source/common/stats:timespan_lib", ], ) diff --git a/mobile/library/common/http/client.cc b/mobile/library/common/http/client.cc index da2c06246f197..d39313ea667d6 100644 --- a/mobile/library/common/http/client.cc +++ b/mobile/library/common/http/client.cc @@ -6,6 +6,7 @@ #include "source/common/http/header_map_impl.h" #include "source/common/http/headers.h" #include "source/common/http/utility.h" +#include "source/common/quic/scone_state.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" @@ -303,7 +304,7 @@ void Client::DirectStreamCallbacks::onError() { // error occurs (e.g., timeout)? if (explicit_flow_control_ && (hasDataToSend() || response_trailers_.get())) { - ENVOY_LOG(debug, "[S{}] defering remote reset stream due to explicit flow control", + ENVOY_LOG(debug, "[S{}] deferring remote reset stream due to explicit flow control", direct_stream_.stream_handle_); if (direct_stream_.parent_.getStream(direct_stream_.stream_handle_, GetStreamFilters::AllowOnlyForOpenStreams)) { @@ -408,6 +409,18 @@ void Client::DirectStream::saveLatestStreamIntel() { } stream_intel_.stream_id = static_cast(stream_handle_); stream_intel_.attempt_count = info.attemptCount().value_or(0); + + const auto* scone_state = + info.filterState().getDataReadOnly(Envoy::Quic::SconeStateKey); + if (scone_state && scone_state->scone_max_kbps.has_value() && + scone_state->timestamp_ms.has_value()) { + // Only update if the new timestamp from scone_state is greater than the last recorded + // timestamp. + if (scone_state->timestamp_ms.value() > stream_intel_.scone_timestamp_ms) { + stream_intel_.scone_max_kbps = scone_state->scone_max_kbps.value(); + stream_intel_.scone_timestamp_ms = scone_state->timestamp_ms.value(); + } + } } void Client::DirectStream::saveFinalStreamIntel() { @@ -459,7 +472,7 @@ void Client::DirectStreamCallbacks::latchError() { !resp_code_details.empty()) { error_msg_details.push_back(absl::StrCat("det: ", std::move(resp_code_details))); } - // The format of the error message propogated to callbacks is: + // The format of the error message propagated to callbacks is: // rc: {value}|ec: {value}|rsp_flags: {value}|http: {value}|det: {value} // // Where envoy_rc is the HTTP response code from StreamInfo::responseCode(). @@ -534,6 +547,13 @@ void Client::DirectStream::dumpState(std::ostream&, int indent_level) const { void Client::startStream(envoy_stream_t new_stream_handle, EnvoyStreamCallbacks&& stream_callbacks, bool explicit_flow_control) { ASSERT(dispatcher_.isThreadSafe()); + + if (!api_listener_) { + ENVOY_LOG(debug, "[S{}] stream can't be started after shutdown.", new_stream_handle); + stream_callbacks.on_cancel_({}, {}); + return; + } + Client::DirectStreamSharedPtr direct_stream{new DirectStream(new_stream_handle, *this)}; direct_stream->explicit_flow_control_ = explicit_flow_control; direct_stream->callbacks_ = @@ -575,7 +595,7 @@ void Client::sendHeaders(envoy_stream_t stream, RequestHeaderMapPtr headers, boo if (headers->getSchemeValue() != "https" && !SystemHelper::getInstance().isCleartextPermitted(headers->getHostValue())) { request_decoder->sendLocalReply(Http::Code::BadRequest, "Cleartext is not permitted", nullptr, - absl::nullopt, ""); + std::nullopt, ""); return; } diff --git a/mobile/library/common/http/client.h b/mobile/library/common/http/client.h index 16238bce2a440..7d14e1e3509ab 100644 --- a/mobile/library/common/http/client.h +++ b/mobile/library/common/http/client.h @@ -17,7 +17,7 @@ #include "source/common/stats/timespan_impl.h" #include "absl/container/flat_hash_map.h" -#include "absl/types/optional.h" +#include #include "library/common/engine_types.h" #include "library/common/event/provisional_dispatcher.h" #include "library/common/network/synthetic_address_impl.h" @@ -54,7 +54,7 @@ class Client : public Logger::Loggable { public: Client(ApiListenerPtr&& api_listener, Event::ProvisionalDispatcher& dispatcher, Stats::Scope& scope, Random::RandomGenerator& random, - absl::optional high_watermark = absl::nullopt) + std::optional high_watermark = std::nullopt) : api_listener_(std::move(api_listener)), dispatcher_(dispatcher), stats_( HttpClientStats{ALL_HTTP_CLIENT_STATS(POOL_COUNTER_PREFIX(scope, "http.client."), @@ -65,6 +65,11 @@ class Client : public Logger::Loggable { // and having local data of 2M + kernel-buffer-limit for HTTP/1.1 random_(random), high_watermark_(high_watermark.value_or(2 * 1024 * 1024)) {} + ~Client() { + ASSERT(api_listener_ == nullptr, + "shutdownApiListener must be called before Client is destructed"); + } + /** * Attempts to open a new stream to the remote. Note that this function is asynchronous and * opening a stream may fail. The returned handle is immediately valid for use with this API, but @@ -139,7 +144,10 @@ class Client : public Logger::Loggable { CONSTRUCT_ON_FIRST_USE(std::string, "client_cancelled_stream"); } - void shutdownApiListener() { api_listener_.reset(); } + void shutdownApiListener() { + ENVOY_LOG(debug, "shutdownApiListener called"); + api_listener_.reset(); + } private: class DirectStream; @@ -168,7 +176,7 @@ class Client : public Logger::Loggable { void encodeData(Buffer::Instance& data, bool end_stream) override; void encodeTrailers(const ResponseTrailerMap& trailers) override; Stream& getStream() override { return direct_stream_; } - Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() override { return absl::nullopt; } + Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() override { return std::nullopt; } void encode1xxHeaders(const ResponseHeaderMap&) override { IS_ENVOY_BUG("Unexpected 100 continue"); // proxy_100_continue_ false by default. } @@ -213,7 +221,7 @@ class Client : public Logger::Loggable { DirectStream& direct_stream_; EnvoyStreamCallbacks stream_callbacks_; Client& http_client_; - absl::optional error_; + std::optional error_; bool success_{}; // Buffered response data when in explicit flow control mode. @@ -280,7 +288,7 @@ class Client : public Logger::Loggable { // ScopeTrackedObject void dumpState(std::ostream& os, int indent_level = 0) const override; - absl::optional codecStreamId() const override { return absl::nullopt; } + std::optional codecStreamId() const override { return std::nullopt; } void setResponseDetails(absl::string_view response_details) { response_details_ = response_details; @@ -349,7 +357,7 @@ class Client : public Logger::Loggable { // read faster than the mobile caller can process it. bool explicit_flow_control_ = false; // Latest intel data retrieved from the StreamInfo. - envoy_stream_intel stream_intel_{-1, -1, 0, 0}; + envoy_stream_intel stream_intel_{-1, -1, 0, 0, -1, -1}; envoy_final_stream_intel envoy_final_stream_intel_{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1}; StreamInfo::BytesMeterSharedPtr bytes_meter_; diff --git a/mobile/library/common/internal_engine.cc b/mobile/library/common/internal_engine.cc index 370a5d8448237..b640ec6c23a17 100644 --- a/mobile/library/common/internal_engine.cc +++ b/mobile/library/common/internal_engine.cc @@ -1,20 +1,29 @@ #include "library/common/internal_engine.h" -#include +#include +#include -#include "source/common/api/os_sys_calls_impl.h" -#include "source/common/common/lock_guard.h" -#include "source/common/common/utility.h" -#include "source/common/http/http_server_properties_cache_manager_impl.h" -#include "source/common/network/io_socket_handle_impl.h" -#include "source/common/runtime/runtime_features.h" +#include #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" +#include "envoy/network/connection_handler.h" +#include "library/common/engine_types.h" +#include "library/common/extensions/listener_managers/api_listener_manager/api_listener_manager.h" +#include "library/common/http/client.h" #include "library/common/mobile_process_wide.h" #include "library/common/network/network_types.h" #include "library/common/network/proxy_api.h" +#include "library/common/network/socket_tag_socket_option_impl.h" #include "library/common/stats/utility.h" +#include "library/common/types/c_types.h" +#include "source/common/api/os_sys_calls_impl.h" +#include "source/common/common/assert.h" +#include "source/common/common/lock_guard.h" +#include "source/common/common/utility.h" +#include "source/common/http/http_server_properties_cache_manager_impl.h" +#include "source/common/network/io_socket_handle_impl.h" +#include "source/common/runtime/runtime_features.h" namespace Envoy { namespace { @@ -81,16 +90,16 @@ static std::atomic current_stream_handle_{0}; InternalEngine::InternalEngine(std::unique_ptr callbacks, std::unique_ptr logger, std::unique_ptr event_tracker, - absl::optional thread_priority, - absl::optional high_watermark, - bool disable_dns_refresh_on_network_change, - Thread::PosixThreadFactoryPtr thread_factory, bool enable_logger) + std::optional thread_priority, + std::optional high_watermark, + Thread::PosixThreadFactoryPtr thread_factory, bool enable_logger, + bool use_worker_thread) : thread_factory_(std::move(thread_factory)), callbacks_(std::move(callbacks)), logger_(std::move(logger)), event_tracker_(std::move(event_tracker)), thread_priority_(thread_priority), high_watermark_(high_watermark), - dispatcher_(std::make_unique()), - disable_dns_refresh_on_network_change_(disable_dns_refresh_on_network_change), - enable_logger_(enable_logger) { + main_dispatcher_(std::make_unique()), + enable_logger_(enable_logger), use_worker_thread_(use_worker_thread), + request_dispatcher_(std::make_unique()) { ExtensionRegistry::registerFactories(); Api::External::registerApi(std::string(ENVOY_EVENT_TRACKER_API_NAME), &event_tracker_); @@ -99,54 +108,123 @@ InternalEngine::InternalEngine(std::unique_ptr callbacks, InternalEngine::InternalEngine(std::unique_ptr callbacks, std::unique_ptr logger, std::unique_ptr event_tracker, - absl::optional thread_priority, - absl::optional high_watermark, - bool disable_dns_refresh_on_network_change, bool enable_logger) + std::optional thread_priority, + std::optional high_watermark, bool enable_logger, + bool use_worker_thread) : InternalEngine(std::move(callbacks), std::move(logger), std::move(event_tracker), - thread_priority, high_watermark, disable_dns_refresh_on_network_change, - Thread::PosixThreadFactory::create(), enable_logger) {} + thread_priority, high_watermark, Thread::PosixThreadFactory::create(), + enable_logger, use_worker_thread) {} envoy_stream_t InternalEngine::initStream() { return current_stream_handle_++; } envoy_status_t InternalEngine::startStream(envoy_stream_t stream, EnvoyStreamCallbacks&& stream_callbacks, - bool explicit_flow_control) { - return dispatcher_->post( - [&, stream, stream_callbacks = std::move(stream_callbacks), explicit_flow_control]() mutable { - http_client_->startStream(stream, std::move(stream_callbacks), explicit_flow_control); - }); + bool explicit_flow_control, + absl::string_view listener_name) { + EnvoyStreamCallbacks wrapped_callbacks; + wrapped_callbacks.on_headers_ = std::move(stream_callbacks.on_headers_); + wrapped_callbacks.on_data_ = std::move(stream_callbacks.on_data_); + wrapped_callbacks.on_trailers_ = std::move(stream_callbacks.on_trailers_); + wrapped_callbacks.on_send_window_available_ = + std::move(stream_callbacks.on_send_window_available_); + + wrapped_callbacks.on_complete_ = + [this, stream, on_complete = std::move(stream_callbacks.on_complete_)]( + envoy_stream_intel stream_intel, envoy_final_stream_intel final_stream_intel) mutable { + removeActiveStream(stream); + if (on_complete) { + on_complete(stream_intel, final_stream_intel); + } + }; + wrapped_callbacks.on_error_ = [this, stream, on_error = std::move(stream_callbacks.on_error_)]( + const EnvoyError& error, envoy_stream_intel stream_intel, + envoy_final_stream_intel final_stream_intel) mutable { + removeActiveStream(stream); + if (on_error) { + on_error(error, stream_intel, final_stream_intel); + } + }; + wrapped_callbacks.on_cancel_ = [this, stream, on_cancel = std::move(stream_callbacks.on_cancel_)]( + envoy_stream_intel stream_intel, + envoy_final_stream_intel final_stream_intel) mutable { + removeActiveStream(stream); + if (on_cancel) { + on_cancel(stream_intel, final_stream_intel); + } + }; + + return requestDispatcher().post([this, stream, wrapped_callbacks = std::move(wrapped_callbacks), + explicit_flow_control, + name = std::string(listener_name)]() mutable { + auto* api_mgr = dynamic_cast(&server_->listenerManager()); + ASSERT(api_mgr != nullptr); + Http::Client* client = api_mgr->httpClient(name); + if (client == nullptr) { + EnvoyError error; + error.error_code_ = ENVOY_STREAM_RESET; + error.message_ = "Listener not found: " + name; + envoy_stream_intel stream_intel{}; + envoy_final_stream_intel final_stream_intel{}; + wrapped_callbacks.on_error_(error, stream_intel, final_stream_intel); + return; + } + active_streams_[stream] = client; + client->startStream(stream, std::move(wrapped_callbacks), explicit_flow_control); + }); } envoy_status_t InternalEngine::sendHeaders(envoy_stream_t stream, Http::RequestHeaderMapPtr headers, bool end_stream, bool idempotent) { - return dispatcher_->post( + return requestDispatcher().post( [this, stream, headers = std::move(headers), end_stream, idempotent]() mutable { - http_client_->sendHeaders(stream, std::move(headers), end_stream, idempotent); + auto it = active_streams_.find(stream); + if (it != active_streams_.end()) { + it->second->sendHeaders(stream, std::move(headers), end_stream, idempotent); + } }); - return ENVOY_SUCCESS; } envoy_status_t InternalEngine::readData(envoy_stream_t stream, size_t bytes_to_read) { - return dispatcher_->post( - [&, stream, bytes_to_read]() { http_client_->readData(stream, bytes_to_read); }); + return requestDispatcher().post([this, stream, bytes_to_read]() { + auto it = active_streams_.find(stream); + if (it != active_streams_.end()) { + it->second->readData(stream, bytes_to_read); + } + }); } envoy_status_t InternalEngine::sendData(envoy_stream_t stream, Buffer::InstancePtr buffer, bool end_stream) { - return dispatcher_->post([&, stream, buffer = std::move(buffer), end_stream]() mutable { - http_client_->sendData(stream, std::move(buffer), end_stream); + return requestDispatcher().post([this, stream, buffer = std::move(buffer), end_stream]() mutable { + auto it = active_streams_.find(stream); + if (it != active_streams_.end()) { + it->second->sendData(stream, std::move(buffer), end_stream); + } }); } envoy_status_t InternalEngine::sendTrailers(envoy_stream_t stream, Http::RequestTrailerMapPtr trailers) { - return dispatcher_->post([&, stream, trailers = std::move(trailers)]() mutable { - http_client_->sendTrailers(stream, std::move(trailers)); + return requestDispatcher().post([this, stream, trailers = std::move(trailers)]() mutable { + auto it = active_streams_.find(stream); + if (it != active_streams_.end()) { + it->second->sendTrailers(stream, std::move(trailers)); + } }); } envoy_status_t InternalEngine::cancelStream(envoy_stream_t stream) { - return dispatcher_->post([&, stream]() { http_client_->cancelStream(stream); }); + return requestDispatcher().post([this, stream]() { + auto it = active_streams_.find(stream); + if (it != active_streams_.end()) { + it->second->cancelStream(stream); + } + }); +} + +void InternalEngine::removeActiveStream(envoy_stream_t stream) { + ASSERT(requestDispatcher().isThreadSafe()); + active_streams_.erase(stream); } // This function takes a `std::shared_ptr` instead of `std::unique_ptr` because `std::function` is a @@ -193,7 +271,16 @@ envoy_status_t InternalEngine::main(std::shared_ptr options) { } } - main_common = std::make_unique(options); + main_common = std::make_unique(options, [this]() { + ASSERT(Thread::MainThread::isMainOrTestThread()); + if (use_worker_thread_) { + ENVOY_LOG_MISC(info, "Worker thread has started."); + auto* api_mgr = + dynamic_cast(&server_->listenerManager()); + ASSERT(api_mgr != nullptr); + request_dispatcher_->drain(api_mgr->httpClientDispatcher()); + } + }); server_ = main_common->server(); event_dispatcher_ = &server_->dispatcher(); @@ -227,7 +314,7 @@ envoy_status_t InternalEngine::main(std::shared_ptr options) { connectivity_manager_ = Network::ConnectivityManagerFactory{generic_context}.get(); Network::DefaultNetworkChangeCallback cb = [this](envoy_netconf_t current_configuration_key) { - dispatcher_->post([this, current_configuration_key]() { + main_dispatcher_->post([this, current_configuration_key]() { if (connectivity_manager_->getConfigurationKey() != current_configuration_key) { // The default network has changed to a different one. return; @@ -261,13 +348,8 @@ envoy_status_t InternalEngine::main(std::shared_ptr options) { // on-the-fly without risking contention on system with lots of threads. // It also comes with ease of programming. stat_name_set_ = client_scope_->symbolTable().makeSet("pulse"); - auto api_listener = server_->listenerManager().apiListener()->get().createHttpApiListener( - server_->dispatcher()); - ASSERT(api_listener != nullptr); - http_client_ = std::make_unique( - std::move(api_listener), *dispatcher_, server_->serverFactoryContext().scope(), - server_->api().randomGenerator(), high_watermark_); - dispatcher_->drain(server_->dispatcher()); + + main_dispatcher_->drain(server_->dispatcher()); engine_running_.Notify(); callbacks_->on_engine_running_(); }); @@ -321,17 +403,21 @@ envoy_status_t InternalEngine::terminate() { } ASSERT(event_dispatcher_); - ASSERT(dispatcher_); - - // We must destroy the Http::ApiListener in the main thread. - dispatcher_->post([this]() { http_client_->shutdownApiListener(); }); - - // Exit the event loop and finish up in Engine::run(...) + ASSERT(main_dispatcher_); if (thread_factory_->currentPthreadId() == main_thread_->pthreadId()) { // TODO(goaway): figure out some way to support this. PANIC("Terminating the engine from its own main thread is currently unsupported."); } else { - dispatcher_->terminate(); + if (!use_worker_thread_) { + main_dispatcher_->post([this]() { + server_->listenerManager().stopListeners(Server::ListenerManager::StopListenersType::All, + Network::ExtraShutdownListenerOptions{}); + }); + } else { + server_->listenerManager().stopListeners(Server::ListenerManager::StopListenersType::All, + Network::ExtraShutdownListenerOptions{}); + } + main_dispatcher_->terminate(); } } // lock(_mutex) @@ -351,13 +437,16 @@ InternalEngine::~InternalEngine() { } envoy_status_t InternalEngine::setProxySettings(absl::string_view hostname, const uint16_t port) { - return dispatcher_->post([&, host = std::string(hostname), port]() -> void { - connectivity_manager_->setProxySettings(Network::ProxySettings::parseHostAndPort(host, port)); + return main_dispatcher_->post([&, host = std::string(hostname), port]() -> void { + if (!use_worker_thread_) { + // Proxy settings are not supported when using worker thread. + connectivity_manager_->setProxySettings(Network::ProxySettings::parseHostAndPort(host, port)); + } }); } envoy_status_t InternalEngine::resetConnectivityState() { - return dispatcher_->post([&]() -> void { connectivity_manager_->resetConnectivityState(); }); + return main_dispatcher_->post([&]() -> void { connectivity_manager_->resetConnectivityState(); }); } void InternalEngine::onDefaultNetworkAvailable() { @@ -368,7 +457,7 @@ void InternalEngine::onDefaultNetworkAvailable() { void InternalEngine::onDefaultNetworkChangeEvent(const int network_type) { ENVOY_LOG_MISC(trace, "Calling the default network change event callback"); - dispatcher_->post([&, network_type]() -> void { + main_dispatcher_->post([&, network_type]() -> void { Network::Address::InstanceConstSharedPtr local_addr = probeAndGetLocalAddr(AF_INET6); const bool has_ipv6_connectivity = local_addr != nullptr; if (local_addr == nullptr) { @@ -395,7 +484,7 @@ void InternalEngine::onDefaultNetworkChangeEvent(const int network_type) { // default. void InternalEngine::onDefaultNetworkChanged(int network) { ENVOY_LOG_MISC(trace, "Calling the default network changed callback"); - dispatcher_->post([&, network]() -> void { + main_dispatcher_->post([&, network]() -> void { handleNetworkChange(network, probeAndGetLocalAddr(AF_INET6) != nullptr); }); } @@ -427,7 +516,7 @@ void InternalEngine::purgeActiveNetworkListAndroid(const std::vector& a void InternalEngine::onDefaultNetworkUnavailable() { ENVOY_LOG_MISC(trace, "Calling the default network unavailable callback"); - dispatcher_->post([&]() -> void { connectivity_manager_->dnsCache()->stop(); }); + main_dispatcher_->post([&]() -> void { connectivity_manager_->dnsCache()->stop(); }); } void InternalEngine::handleNetworkChange(const int network_type, const bool has_ipv6_connectivity) { @@ -450,7 +539,7 @@ void InternalEngine::resetHttpPropertiesAndDrainHosts(bool has_ipv6_connectivity if (!has_ipv6_connectivity) { connectivity_manager_->dnsCache()->setIpVersionToRemove({Network::Address::IpVersion::v6}); } else { - connectivity_manager_->dnsCache()->setIpVersionToRemove(absl::nullopt); + connectivity_manager_->dnsCache()->setIpVersionToRemove(std::nullopt); } } Http::HttpServerPropertiesCacheManager& cache_manager = @@ -480,7 +569,7 @@ void InternalEngine::resetHttpPropertiesAndDrainHosts(bool has_ipv6_connectivity envoy_status_t InternalEngine::recordCounterInc(absl::string_view elements, envoy_stats_tags tags, uint64_t count) { - return dispatcher_->post( + return main_dispatcher_->post( [&, name = Stats::Utility::sanitizeStatsName(elements), tags, count]() -> void { ENVOY_LOG(trace, "[pulse.{}] recordCounterInc", name); Stats::StatNameTagVector tags_vctr = @@ -490,7 +579,7 @@ envoy_status_t InternalEngine::recordCounterInc(absl::string_view elements, envo }); } -Event::ProvisionalDispatcher& InternalEngine::dispatcher() const { return *dispatcher_; } +Event::ProvisionalDispatcher& InternalEngine::dispatcher() const { return *main_dispatcher_; } Thread::PosixThreadFactory& InternalEngine::threadFactory() const { return *thread_factory_; } @@ -540,7 +629,7 @@ std::string InternalEngine::dumpStats() { std::string stats; absl::Notification stats_received; - if (dispatcher_->post([&]() -> void { + if (main_dispatcher_->post([&]() -> void { Envoy::Buffer::OwnedImpl instance; handlerStats(server_->stats(), instance); stats = instance.toString(); @@ -552,14 +641,45 @@ std::string InternalEngine::dumpStats() { return stats; } +void InternalEngine::drainConnectionsBySocketTag(uint32_t tag) { + if (!main_thread_->joinable() || terminated_) { + return; + } + std::vector target_hash; + uid_t uid = 0; + Network::SocketTagSocketOptionImpl::generateHashKey(uid, tag, target_hash); + + main_dispatcher_->post([this, target_hash]() { + auto& cluster_manager = getClusterManager(); + cluster_manager.drainOrCloseConnPools( + [target_hash](ConnectionPool::Instance& pool) { + const auto& options = pool.socketOptions(); + if (!options) + return false; + for (const auto& option : *options) { + std::vector option_hash; + option->hashKey(option_hash); + if (option_hash == target_hash) { + ASSERT(dynamic_cast(option.get()) != + nullptr); + return true; + } + } + return false; + }, + ConnectionPool::DrainBehavior::DrainAndDelete); + }); +} + Upstream::ClusterManager& InternalEngine::getClusterManager() { - ASSERT(dispatcher_->isThreadSafe(), + ASSERT(main_dispatcher_->isThreadSafe(), "getClusterManager must be called from the dispatcher's context"); return server_->clusterManager(); } Stats::Store& InternalEngine::getStatsStore() { - ASSERT(dispatcher_->isThreadSafe(), "getStatsStore must be called from the dispatcher's context"); + ASSERT(main_dispatcher_->isThreadSafe(), + "getStatsStore must be called from the dispatcher's context"); return server_->stats(); } diff --git a/mobile/library/common/internal_engine.h b/mobile/library/common/internal_engine.h index 6550d42aee917..64f1b00eb6e9e 100644 --- a/mobile/library/common/internal_engine.h +++ b/mobile/library/common/internal_engine.h @@ -10,7 +10,7 @@ #include "absl/strings/string_view.h" #include "absl/synchronization/notification.h" -#include "absl/types/optional.h" +#include #include "extension_registry.h" #include "library/common/engine_common.h" #include "library/common/engine_types.h" @@ -34,9 +34,9 @@ class InternalEngine : public Logger::Loggable { */ InternalEngine(std::unique_ptr callbacks, std::unique_ptr logger, std::unique_ptr event_tracker, - absl::optional thread_priority = absl::nullopt, - absl::optional high_watermark = absl::nullopt, - bool disable_dns_refresh_on_network_change = false, bool enable_logger = true); + std::optional thread_priority = std::nullopt, + std::optional high_watermark = std::nullopt, bool enable_logger = true, + bool use_worker_thread = false); /** * InternalEngine destructor. @@ -75,7 +75,7 @@ class InternalEngine : public Logger::Loggable { // to http client functions of the same name after doing a dispatcher post // (thread context switch) envoy_status_t startStream(envoy_stream_t stream, EnvoyStreamCallbacks&& stream_callbacks, - bool explicit_flow_control); + bool explicit_flow_control, absl::string_view listener_name = ""); /** * Send the headers over an open HTTP stream. This function can be invoked @@ -193,6 +193,7 @@ class InternalEngine : public Logger::Loggable { * Dumps Envoy stats into string. Returns an empty string when an error occurred. */ std::string dumpStats(); + void drainConnectionsBySocketTag(uint32_t tag); /** * Get cluster manager from the Engine. @@ -204,15 +205,22 @@ class InternalEngine : public Logger::Loggable { */ Stats::Store& getStatsStore(); + /** + * Set whether to disable DNS refresh on network change events. + */ + void disableDnsRefreshOnNetworkChange(bool disable) { + disable_dns_refresh_on_network_change_ = disable; + } + private: // Needs access to the private constructor. GTEST_FRIEND_CLASS(InternalEngineTest, ThreadCreationFailed); InternalEngine(std::unique_ptr callbacks, std::unique_ptr logger, std::unique_ptr event_tracker, - absl::optional thread_priority, absl::optional high_watermark, - bool disable_dns_refresh_on_network_change, - Thread::PosixThreadFactoryPtr thread_factory, bool enable_logger = true); + std::optional thread_priority, std::optional high_watermark, + Thread::PosixThreadFactoryPtr thread_factory, bool enable_logger = true, + bool use_worker_thread = false); envoy_status_t main(std::shared_ptr options); static void logInterfaces(absl::string_view event, @@ -234,6 +242,12 @@ class InternalEngine : public Logger::Loggable { // Called when it's been determined that the default network has changed. void resetHttpPropertiesAndDrainHosts(bool has_ipv6_connectivity); + void removeActiveStream(envoy_stream_t stream); + + Event::ProvisionalDispatcher& requestDispatcher() const { + return use_worker_thread_ ? *request_dispatcher_ : *main_dispatcher_; + } + Thread::PosixThreadFactoryPtr thread_factory_; Event::Dispatcher* event_dispatcher_{}; Stats::ScopeSharedPtr client_scope_; @@ -241,15 +255,15 @@ class InternalEngine : public Logger::Loggable { std::unique_ptr callbacks_; std::unique_ptr logger_; std::unique_ptr event_tracker_; - absl::optional thread_priority_; - absl::optional high_watermark_; + std::optional thread_priority_; + std::optional high_watermark_; Assert::ActionRegistrationPtr assert_handler_registration_; Assert::ActionRegistrationPtr bug_handler_registration_; Thread::MutexBasicLockable mutex_; Thread::CondVar cv_; - Http::ClientPtr http_client_; + absl::flat_hash_map active_streams_; Network::ConnectivityManagerImplSharedPtr connectivity_manager_; - Event::ProvisionalDispatcherPtr dispatcher_; + Event::ProvisionalDispatcherPtr main_dispatcher_; // Used by the cerr logger to ensure logs don't overwrite each other. absl::Mutex log_mutex_; Logger::EventTrackingDelegatePtr log_delegate_ptr_{}; @@ -260,10 +274,14 @@ class InternalEngine : public Logger::Loggable { Thread::PosixThreadPtr main_thread_{nullptr}; // Empty placeholder to be populated later. bool terminated_{false}; absl::Notification engine_running_; - bool disable_dns_refresh_on_network_change_; + bool disable_dns_refresh_on_network_change_{false}; int prev_network_type_{0}; Network::Address::InstanceConstSharedPtr prev_local_addr_{nullptr}; bool enable_logger_{true}; + bool use_worker_thread_{false}; + // If use_worker_thread_ is true, request_dispatcher_ will point to the dispatcher_ in the + // ApiListenerWorker. Otherwise, it will be null. + Event::ProvisionalDispatcherPtr request_dispatcher_; }; } // namespace Envoy diff --git a/mobile/library/common/network/BUILD b/mobile/library/common/network/BUILD index e593e80066838..3a1201956bdd1 100644 --- a/mobile/library/common/network/BUILD +++ b/mobile/library/common/network/BUILD @@ -37,6 +37,7 @@ envoy_cc_library( repository = "@envoy", deps = [ ":network_types_lib", + "@abseil-cpp//absl/synchronization", "@envoy//source/common/quic:envoy_quic_network_observer_registry_factory_lib", ], ) @@ -65,6 +66,7 @@ envoy_cc_library( "@envoy//source/common/common:scalar_to_byte_vector_lib", "@envoy//source/common/network:addr_family_aware_socket_option_lib", "@envoy//source/common/network:socket_option_lib", + "@envoy//source/common/runtime:runtime_features_lib", "@envoy//source/extensions/common/dynamic_forward_proxy:dns_cache_manager_impl", ], ) diff --git a/mobile/library/common/network/apple_proxy_resolver.cc b/mobile/library/common/network/apple_proxy_resolver.cc index 2c36eba5e6c07..65a3bd5c0a1c6 100644 --- a/mobile/library/common/network/apple_proxy_resolver.cc +++ b/mobile/library/common/network/apple_proxy_resolver.cc @@ -22,11 +22,10 @@ AppleProxyResolver::~AppleProxyResolver() { } SystemProxySettingsReadCallback AppleProxyResolver::proxySettingsUpdater() { - return SystemProxySettingsReadCallback( - [this](absl::optional proxy_settings) { - absl::MutexLock l(mutex_); - proxy_settings_ = std::move(proxy_settings); - }); + return SystemProxySettingsReadCallback([this](std::optional proxy_settings) { + absl::MutexLock l(mutex_); + proxy_settings_ = std::move(proxy_settings); + }); } void AppleProxyResolver::start() { @@ -42,6 +41,7 @@ AppleProxyResolver::resolveProxy(const std::string& target_url_string, ProxySettingsResolvedCallback proxy_resolution_completed) { ASSERT(started_, "AppleProxyResolver not started."); ASSERT(dispatcher_ != nullptr, "Dispatcher not set on the AppleProxyResolver."); + ASSERT(dispatcher_->isThreadSafe(), "Proxy resolution must happen on the main thread."); std::string pac_file_url; { @@ -85,7 +85,7 @@ AppleProxyResolver::resolveProxy(const std::string& target_url_string, }); }); }, - /* options= */ absl::nullopt, /* crash_on_failure=*/false); + /* options= */ std::nullopt, /* crash_on_failure=*/false); Thread::ThreadId thread_id = thread->pthreadId(); pac_resolution_threads_[thread_id] = std::move(thread); diff --git a/mobile/library/common/network/apple_proxy_resolver.h b/mobile/library/common/network/apple_proxy_resolver.h index 2084951e0a4e6..42e69dfa5ad3d 100644 --- a/mobile/library/common/network/apple_proxy_resolver.h +++ b/mobile/library/common/network/apple_proxy_resolver.h @@ -56,7 +56,7 @@ class AppleProxyResolver : public ProxyResolver { std::unique_ptr proxy_settings_monitor_; std::unique_ptr pac_proxy_resolver_; - absl::optional proxy_settings_; + std::optional proxy_settings_; std::unique_ptr thread_factory_; absl::flat_hash_map pac_resolution_threads_; Event::Dispatcher* dispatcher_; diff --git a/mobile/library/common/network/apple_system_proxy_settings_monitor.cc b/mobile/library/common/network/apple_system_proxy_settings_monitor.cc index a61d830c3054f..6ce433689dc5e 100644 --- a/mobile/library/common/network/apple_system_proxy_settings_monitor.cc +++ b/mobile/library/common/network/apple_system_proxy_settings_monitor.cc @@ -27,7 +27,7 @@ void AppleSystemProxySettingsMonitor::start() { queue_ = dispatch_queue_create("io.envoyproxy.envoymobile.AppleSystemProxySettingsMonitor", attributes); - __block absl::optional proxy_settings; + __block std::optional proxy_settings; dispatch_sync(queue_, ^{ proxy_settings = readSystemProxySettings(); proxy_settings_read_callback_(std::move(proxy_settings)); @@ -59,11 +59,11 @@ CFDictionaryRef AppleSystemProxySettingsMonitor::getSystemProxySettings() const return CFNetworkCopySystemProxySettings(); } -absl::optional +std::optional AppleSystemProxySettingsMonitor::readSystemProxySettings() const { CFDictionaryRef proxy_settings = getSystemProxySettings(); if (proxy_settings == nullptr) { - return absl::nullopt; + return std::nullopt; } // iOS system settings allow users to enter an arbitrary big integer number i.e. 88888888 as a @@ -76,7 +76,7 @@ AppleSystemProxySettingsMonitor::readSystemProxySettings() const { const bool is_http_proxy_enabled = Apple::toInt(cf_is_http_proxy_enabled) > 0; const bool is_auto_config_proxy_enabled = Apple::toInt(cf_is_auto_config_proxy_enabled) > 0; - absl::optional settings; + std::optional settings; if (is_http_proxy_enabled) { CFStringRef cf_hostname = static_cast(CFDictionaryGetValue(proxy_settings, kCFNetworkProxiesHTTPProxy)); @@ -85,14 +85,14 @@ AppleSystemProxySettingsMonitor::readSystemProxySettings() const { static_cast(CFDictionaryGetValue(proxy_settings, kCFNetworkProxiesHTTPPort)); int port = Apple::toInt(cf_port); if (!hostname.empty() && port > 0) { - settings = absl::make_optional(std::move(hostname), port); + settings = std::make_optional(std::move(hostname), port); } } else if (is_auto_config_proxy_enabled) { CFStringRef cf_pac_file_url_string = static_cast( CFDictionaryGetValue(proxy_settings, kCFNetworkProxiesProxyAutoConfigURLString)); std::string pac_file_url_str = Apple::toString(cf_pac_file_url_string); if (!pac_file_url_str.empty()) { - settings = absl::make_optional(std::move(pac_file_url_str)); + settings = std::make_optional(std::move(pac_file_url_str)); } } diff --git a/mobile/library/common/network/apple_system_proxy_settings_monitor.h b/mobile/library/common/network/apple_system_proxy_settings_monitor.h index 2e38f6a92f141..6da54193c9cd8 100644 --- a/mobile/library/common/network/apple_system_proxy_settings_monitor.h +++ b/mobile/library/common/network/apple_system_proxy_settings_monitor.h @@ -6,7 +6,7 @@ #include -#include "absl/types/optional.h" +#include #include "library/common/network/proxy_settings.h" namespace Envoy { @@ -41,7 +41,7 @@ class AppleSystemProxySettingsMonitor { virtual CFDictionaryRef getSystemProxySettings() const; private: - absl::optional readSystemProxySettings() const; + std::optional readSystemProxySettings() const; dispatch_source_t source_; dispatch_queue_t queue_; diff --git a/mobile/library/common/network/connectivity_manager.cc b/mobile/library/common/network/connectivity_manager.cc index 9f87acaf053f2..7fa2549232215 100644 --- a/mobile/library/common/network/connectivity_manager.cc +++ b/mobile/library/common/network/connectivity_manager.cc @@ -13,6 +13,7 @@ #include "source/common/common/utility.h" #include "source/common/network/addr_family_aware_socket_option_impl.h" #include "source/common/network/address_impl.h" +#include "source/common/runtime/runtime_features.h" #include "source/extensions/common/dynamic_forward_proxy/dns_cache_manager_impl.h" #include "fmt/ostream.h" @@ -637,6 +638,7 @@ ConnectivityManagerImplSharedPtr ConnectivityManagerFactory::get() { SINGLETON_MANAGER_REGISTERED_NAME(connectivity_manager), [this] { Envoy::Extensions::Common::DynamicForwardProxy::DnsCacheManagerFactoryImpl cache_manager_factory{context_}; + return std::make_shared( context_.serverFactoryContext().clusterManager(), cache_manager_factory.get()); }); diff --git a/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.cc b/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.cc index aef64a676b891..e19c8e2bb3cf7 100644 --- a/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.cc +++ b/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.cc @@ -65,14 +65,29 @@ bool EnvoyMobileQuicNetworkObserverRegistry::isNetworkChangeUpToDate( } } +EnvoyMobileQuicNetworkObserverRegistryFactory::EnvoyMobileQuicNetworkObserverRegistryFactory( + NetworkConnectivityTracker& network_tracker) + : network_tracker_(network_tracker) { + thread_local_observer_registries_.reserve(1); +} + EnvoyQuicNetworkObserverRegistryPtr EnvoyMobileQuicNetworkObserverRegistryFactory::createQuicNetworkObserverRegistry( Event::Dispatcher& dispatcher) { auto result = std::make_unique(dispatcher, network_tracker_); - thread_local_observer_registries_.emplace_back(*result); + { + absl::WriterMutexLock lock(&mutex_); + thread_local_observer_registries_.emplace_back(*result); + } return result; } +std::vector> +EnvoyMobileQuicNetworkObserverRegistryFactory::getCreatedObserverRegistries() { + absl::ReaderMutexLock lock(&mutex_); + return thread_local_observer_registries_; +} + } // namespace Quic } // namespace Envoy diff --git a/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.h b/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.h index 80916590259cd..aa032495b6a26 100644 --- a/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.h +++ b/mobile/library/common/network/envoy_mobile_quic_network_observer_registry_factory.h @@ -3,6 +3,8 @@ #include "source/common/quic/envoy_quic_network_observer_registry_factory.h" #include "source/common/quic/quic_network_connectivity_observer.h" +#include "absl/synchronization/mutex.h" + #include "library/common/network/network_types.h" namespace Envoy { @@ -66,22 +68,18 @@ class EnvoyMobileQuicNetworkObserverRegistryFactory : public EnvoyQuicNetworkObserverRegistryFactory { public: explicit EnvoyMobileQuicNetworkObserverRegistryFactory( - NetworkConnectivityTracker& network_tracker) - : network_tracker_(network_tracker) { - thread_local_observer_registries_.reserve(1); - } + NetworkConnectivityTracker& network_tracker); EnvoyQuicNetworkObserverRegistryPtr createQuicNetworkObserverRegistry(Event::Dispatcher& dispatcher) override; - std::vector>& - getCreatedObserverRegistries() { - return thread_local_observer_registries_; - } + std::vector> + getCreatedObserverRegistries(); private: + absl::Mutex mutex_; std::vector> - thread_local_observer_registries_; + thread_local_observer_registries_ ABSL_GUARDED_BY(mutex_); NetworkConnectivityTracker& network_tracker_; }; diff --git a/mobile/library/common/network/network_type_socket_option_impl.cc b/mobile/library/common/network/network_type_socket_option_impl.cc index 64e05019586ce..4b1d4bde86996 100644 --- a/mobile/library/common/network/network_type_socket_option_impl.cc +++ b/mobile/library/common/network/network_type_socket_option_impl.cc @@ -5,7 +5,7 @@ #include "source/common/common/scalar_to_byte_vector.h" -#include "absl/types/optional.h" +#include namespace Envoy { namespace Network { @@ -22,14 +22,14 @@ void NetworkTypeSocketOptionImpl::hashKey(std::vector& hash_key) const pushScalarToByteVector(network_type_, hash_key); } -absl::optional NetworkTypeSocketOptionImpl::getOptionDetails( +std::optional NetworkTypeSocketOptionImpl::getOptionDetails( const Socket&, envoy::config::core::v3::SocketOption::SocketState /*state*/) const { Socket::Option::Details details; details.name_ = optname_; std::vector data; pushScalarToByteVector(network_type_, data); details.value_ = std::string(reinterpret_cast(data.data()), data.size()); - return absl::make_optional(std::move(details)); + return std::make_optional(std::move(details)); } bool NetworkTypeSocketOptionImpl::isSupported() const { return true; } diff --git a/mobile/library/common/network/network_type_socket_option_impl.h b/mobile/library/common/network/network_type_socket_option_impl.h index dd2480cd3ffbf..32a6a46d5c744 100644 --- a/mobile/library/common/network/network_type_socket_option_impl.h +++ b/mobile/library/common/network/network_type_socket_option_impl.h @@ -3,7 +3,7 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/network/socket.h" -#include "absl/types/optional.h" +#include namespace Envoy { namespace Network { @@ -21,7 +21,7 @@ class NetworkTypeSocketOptionImpl : public Network::Socket::Option { bool setOption(Network::Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const override; void hashKey(std::vector& hash_key) const override; - absl::optional
+ std::optional
getOptionDetails(const Network::Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const override; bool isSupported() const override; diff --git a/mobile/library/common/network/proxy_settings.h b/mobile/library/common/network/proxy_settings.h index 0f7cc69ec15bc..fc985c1d96cd1 100644 --- a/mobile/library/common/network/proxy_settings.h +++ b/mobile/library/common/network/proxy_settings.h @@ -7,7 +7,7 @@ #include "source/common/network/utility.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" +#include namespace Envoy { namespace Network { @@ -16,7 +16,7 @@ class ProxySettings; class SystemProxySettings; using ProxySettingsConstSharedPtr = std::shared_ptr; using ProxySettingsResolvedCallback = std::function&)>; -using SystemProxySettingsReadCallback = std::function)>; +using SystemProxySettingsReadCallback = std::function)>; /** * Proxy settings coming from platform specific APIs, i.e. ConnectivityManager in diff --git a/mobile/library/common/network/socket_tag_socket_option_impl.cc b/mobile/library/common/network/socket_tag_socket_option_impl.cc index 8d7de6bac390d..7c7548b9cca22 100644 --- a/mobile/library/common/network/socket_tag_socket_option_impl.cc +++ b/mobile/library/common/network/socket_tag_socket_option_impl.cc @@ -31,15 +31,20 @@ bool SocketTagSocketOptionImpl::setOption( return true; } +void SocketTagSocketOptionImpl::generateHashKey(uid_t uid, uint32_t traffic_stats_tag, + std::vector& hash_key) { + pushScalarToByteVector(uid, hash_key); + pushScalarToByteVector(traffic_stats_tag, hash_key); +} + void SocketTagSocketOptionImpl::hashKey(std::vector& hash_key) const { - pushScalarToByteVector(uid_, hash_key); - pushScalarToByteVector(traffic_stats_tag_, hash_key); + generateHashKey(uid_, traffic_stats_tag_, hash_key); } -absl::optional SocketTagSocketOptionImpl::getOptionDetails( +std::optional SocketTagSocketOptionImpl::getOptionDetails( const Socket&, envoy::config::core::v3::SocketOption::SocketState /*state*/) const { if (!isSupported()) { - return absl::nullopt; + return std::nullopt; } static std::string name = "socket_tag"; @@ -49,7 +54,7 @@ absl::optional SocketTagSocketOptionImpl::getOptionDeta pushScalarToByteVector(uid_, data); pushScalarToByteVector(traffic_stats_tag_, data); details.value_ = std::string(reinterpret_cast(data.data()), data.size()); - return absl::make_optional(std::move(details)); + return std::make_optional(std::move(details)); } bool SocketTagSocketOptionImpl::isSupported() const { return optname_.hasValue(); } diff --git a/mobile/library/common/network/socket_tag_socket_option_impl.h b/mobile/library/common/network/socket_tag_socket_option_impl.h index a118c890b31df..d0fe434ea1c46 100644 --- a/mobile/library/common/network/socket_tag_socket_option_impl.h +++ b/mobile/library/common/network/socket_tag_socket_option_impl.h @@ -18,8 +18,10 @@ class SocketTagSocketOptionImpl : public Network::Socket::Option { // Socket::Option bool setOption(Network::Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const override; + static void generateHashKey(uid_t uid, uint32_t traffic_stats_tag, + std::vector& hash_key); void hashKey(std::vector& hash_key) const override; - absl::optional
+ std::optional
getOptionDetails(const Network::Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const override; bool isSupported() const override; diff --git a/mobile/library/common/network/src_addr_socket_option_impl.cc b/mobile/library/common/network/src_addr_socket_option_impl.cc index 108767edb25b2..13303e6d84986 100644 --- a/mobile/library/common/network/src_addr_socket_option_impl.cc +++ b/mobile/library/common/network/src_addr_socket_option_impl.cc @@ -47,10 +47,10 @@ void SrcAddrSocketOptionImpl::hashKey(std::vector& key) const { } } -absl::optional SrcAddrSocketOptionImpl::getOptionDetails( +std::optional SrcAddrSocketOptionImpl::getOptionDetails( const Network::Socket&, envoy::config::core::v3::SocketOption::SocketState) const { // no details for this option. - return absl::nullopt; + return std::nullopt; } } // namespace Network diff --git a/mobile/library/common/network/src_addr_socket_option_impl.h b/mobile/library/common/network/src_addr_socket_option_impl.h index 48ae1d3502876..c48e3bedbe6a8 100644 --- a/mobile/library/common/network/src_addr_socket_option_impl.h +++ b/mobile/library/common/network/src_addr_socket_option_impl.h @@ -33,7 +33,7 @@ class SrcAddrSocketOptionImpl : public Network::Socket::Option { */ void hashKey(std::vector& key) const override; - absl::optional
+ std::optional
getOptionDetails(const Network::Socket& socket, envoy::config::core::v3::SocketOption::SocketState state) const override; bool isSupported() const override { return true; } diff --git a/mobile/library/common/network/synthetic_address_impl.h b/mobile/library/common/network/synthetic_address_impl.h index 2efb138b250c1..fb786b3e0ce70 100644 --- a/mobile/library/common/network/synthetic_address_impl.h +++ b/mobile/library/common/network/synthetic_address_impl.h @@ -54,7 +54,7 @@ class SyntheticAddressImpl : public Instance { return SocketInterfaceSingleton::get(); } - absl::optional networkNamespace() const override { return absl::nullopt; } + std::optional networkNamespace() const override { return std::nullopt; } InstanceConstSharedPtr withNetworkNamespace(absl::string_view) const override { return nullptr; } diff --git a/mobile/library/common/stream_info/extra_stream_info.cc b/mobile/library/common/stream_info/extra_stream_info.cc index 25f60f81010d9..601f608bf1ed0 100644 --- a/mobile/library/common/stream_info/extra_stream_info.cc +++ b/mobile/library/common/stream_info/extra_stream_info.cc @@ -6,8 +6,7 @@ namespace Envoy { namespace StreamInfo { namespace { -void setFromOptional(int64_t& to_set, const absl::optional& time, - int64_t offset_ms) { +void setFromOptional(int64_t& to_set, const std::optional& time, int64_t offset_ms) { if (time.has_value()) { to_set = offset_ms + std::chrono::duration_cast(time.value().time_since_epoch()) diff --git a/mobile/library/common/stream_info/extra_stream_info.h b/mobile/library/common/stream_info/extra_stream_info.h index 2e7f60b66f400..e1f76438a447e 100644 --- a/mobile/library/common/stream_info/extra_stream_info.h +++ b/mobile/library/common/stream_info/extra_stream_info.h @@ -11,7 +11,7 @@ namespace Envoy { namespace StreamInfo { struct ExtraStreamInfo : public FilterState::Object { - absl::optional configuration_key_{}; + std::optional configuration_key_{}; static const std::string& key(); }; diff --git a/mobile/library/common/types/c_types.h b/mobile/library/common/types/c_types.h index 40ef2f0f488ea..3a0a684191ebc 100644 --- a/mobile/library/common/types/c_types.h +++ b/mobile/library/common/types/c_types.h @@ -154,6 +154,10 @@ typedef struct { // of bytes received before decompression. consumed_bytes_from_response omits the number // number of bytes related to the Status Line, and is after decompression. uint64_t consumed_bytes_from_response; + // The latest SCONE maximum bitrate received from the network, in kbps. + int64_t scone_max_kbps; + // Time since epoch when SCONE value was received, -1 if no new value + int64_t scone_timestamp_ms; } envoy_stream_intel; /** diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java index cb7c0d940781e..fe92feadf7a88 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/AndroidEngineImpl.java @@ -67,6 +67,11 @@ public String dumpStats() { return envoyEngine.dumpStats(); } + @Override + public void drainConnectionsBySocketTag(int tag) { + envoyEngine.drainConnectionsBySocketTag(tag); + } + @Override public long getEngineHandle() { return envoyEngine.getEngineHandle(); diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java index a6f5c22817675..b45f5b1549264 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java @@ -38,6 +38,7 @@ public enum TrustChainVerification { public final int dnsNumRetries; public final boolean enableDrainPostDnsRefresh; public final boolean enableHttp3; + public final boolean enableEarlyData; public final String http3ConnectionOptions; public final String http3ClientConnectionOptions; public final Map quicHints; @@ -149,14 +150,14 @@ public EnvoyConfiguration( int dnsFailureRefreshSecondsBase, int dnsFailureRefreshSecondsMax, int dnsQueryTimeoutSeconds, int dnsMinRefreshSeconds, List dnsPreresolveHostnames, boolean enableDNSCache, int dnsCacheSaveIntervalSeconds, int dnsNumRetries, boolean enableDrainPostDnsRefresh, - boolean enableHttp3, String http3ConnectionOptions, String http3ClientConnectionOptions, - Map quicHints, List quicCanonicalSuffixes, - boolean enableGzipDecompression, boolean enableBrotliDecompression, - int numTimeoutsToTriggerPortMigration, boolean enableSocketTagging, - boolean enableInterfaceBinding, int h2ConnectionKeepaliveIdleIntervalMilliseconds, - int h2ConnectionKeepaliveTimeoutSeconds, int maxConnectionsPerHost, - int streamIdleTimeoutSeconds, int perTryIdleTimeoutSeconds, String appVersion, String appId, - TrustChainVerification trustChainVerification, + boolean enableHttp3, boolean enableEarlyData, String http3ConnectionOptions, + String http3ClientConnectionOptions, Map quicHints, + List quicCanonicalSuffixes, boolean enableGzipDecompression, + boolean enableBrotliDecompression, int numTimeoutsToTriggerPortMigration, + boolean enableSocketTagging, boolean enableInterfaceBinding, + int h2ConnectionKeepaliveIdleIntervalMilliseconds, int h2ConnectionKeepaliveTimeoutSeconds, + int maxConnectionsPerHost, int streamIdleTimeoutSeconds, int perTryIdleTimeoutSeconds, + String appVersion, String appId, TrustChainVerification trustChainVerification, List nativeFilterChain, List httpPlatformFilterFactories, Map stringAccessors, @@ -180,6 +181,7 @@ public EnvoyConfiguration( this.dnsNumRetries = dnsNumRetries; this.enableDrainPostDnsRefresh = enableDrainPostDnsRefresh; this.enableHttp3 = enableHttp3; + this.enableEarlyData = enableEarlyData; this.http3ConnectionOptions = http3ConnectionOptions; this.http3ClientConnectionOptions = http3ClientConnectionOptions; this.quicHints = new HashMap<>(); @@ -247,14 +249,14 @@ public long createBootstrap() { dnsRefreshSeconds, dnsFailureRefreshSecondsBase, dnsFailureRefreshSecondsMax, dnsQueryTimeoutSeconds, dnsMinRefreshSeconds, dnsPreresolve, enableDNSCache, dnsCacheSaveIntervalSeconds, dnsNumRetries, enableDrainPostDnsRefresh, enableHttp3, - http3ConnectionOptions, http3ClientConnectionOptions, quicHints, quicSuffixes, - enableGzipDecompression, enableBrotliDecompression, numTimeoutsToTriggerPortMigration, - enableSocketTagging, enableInterfaceBinding, h2ConnectionKeepaliveIdleIntervalMilliseconds, - h2ConnectionKeepaliveTimeoutSeconds, maxConnectionsPerHost, streamIdleTimeoutSeconds, - perTryIdleTimeoutSeconds, appVersion, appId, enforceTrustChainVerification, filterChain, - enablePlatformCertificatesValidation, upstreamTlsSni, runtimeGuards, - h3ConnectionKeepaliveInitialIntervalMilliseconds, useQuicPlatformPacketWriter, - enableQuicConnectionMigration, migrateIdleQuicConnection, + enableEarlyData, http3ConnectionOptions, http3ClientConnectionOptions, quicHints, + quicSuffixes, enableGzipDecompression, enableBrotliDecompression, + numTimeoutsToTriggerPortMigration, enableSocketTagging, enableInterfaceBinding, + h2ConnectionKeepaliveIdleIntervalMilliseconds, h2ConnectionKeepaliveTimeoutSeconds, + maxConnectionsPerHost, streamIdleTimeoutSeconds, perTryIdleTimeoutSeconds, appVersion, + appId, enforceTrustChainVerification, filterChain, enablePlatformCertificatesValidation, + upstreamTlsSni, runtimeGuards, h3ConnectionKeepaliveInitialIntervalMilliseconds, + useQuicPlatformPacketWriter, enableQuicConnectionMigration, migrateIdleQuicConnection, maxIdleTimeBeforeQuicMigrationSeconds, maxTimeOnNonDefaultNetworkSeconds); } } diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java index fcdbf58c4d61b..e2c44b4a4274a 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngine.java @@ -57,6 +57,13 @@ public interface EnvoyEngine { String dumpStats(); + /** + * Drains connections matching the given socket tag. + * + * @param tag Socket tag. + */ + void drainConnectionsBySocketTag(int tag); + /** * Returns a handle to the underlying InternalEngine pointer. * diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index 1b7bddec3b8eb..da07e5ed3fa8d 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -59,6 +59,12 @@ public String dumpStats() { return JniLibrary.dumpStats(engineHandle); } + @Override + public void drainConnectionsBySocketTag(int tag) { + checkIsTerminated(); + JniLibrary.drainConnectionsBySocketTag(engineHandle, tag); + } + @Override public long getEngineHandle() { checkIsTerminated(); diff --git a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index 1390696bf64d3..6c4730fdcae69 100644 --- a/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/mobile/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -201,6 +201,13 @@ protected static native int recordCounterInc(long engine, String elements, byte[ */ protected static native String dumpStats(long engine); + /** + * Drains connections matching the given socket tag. + * @param engine, handle to the engine. + * @param tag, socket tag. + */ + protected static native void drainConnectionsBySocketTag(long engine, int tag); + /** * Register a platform-provided key-value store implementation. * @@ -339,14 +346,14 @@ public static native long createBootstrap( long dnsFailureRefreshSecondsBase, long dnsFailureRefreshSecondsMax, long dnsQueryTimeoutSeconds, long dnsMinRefreshSeconds, byte[][] dnsPreresolveHostnames, boolean enableDNSCache, long dnsCacheSaveIntervalSeconds, int dnsNumRetries, - boolean enableDrainPostDnsRefresh, boolean enableHttp3, String http3ConnectionOptions, - String http3ClientConnectionOptions, byte[][] quicHints, byte[][] quicCanonicalSuffixes, - boolean enableGzipDecompression, boolean enableBrotliDecompression, - int numTimeoutsToTriggerPortMigration, boolean enableSocketTagging, - boolean enableInterfaceBinding, long h2ConnectionKeepaliveIdleIntervalMilliseconds, - long h2ConnectionKeepaliveTimeoutSeconds, long maxConnectionsPerHost, - long streamIdleTimeoutSeconds, long perTryIdleTimeoutSeconds, String appVersion, String appId, - boolean trustChainVerification, byte[][] filterChain, + boolean enableDrainPostDnsRefresh, boolean enableHttp3, boolean enableEarlyData, + String http3ConnectionOptions, String http3ClientConnectionOptions, byte[][] quicHints, + byte[][] quicCanonicalSuffixes, boolean enableGzipDecompression, + boolean enableBrotliDecompression, int numTimeoutsToTriggerPortMigration, + boolean enableSocketTagging, boolean enableInterfaceBinding, + long h2ConnectionKeepaliveIdleIntervalMilliseconds, long h2ConnectionKeepaliveTimeoutSeconds, + long maxConnectionsPerHost, long streamIdleTimeoutSeconds, long perTryIdleTimeoutSeconds, + String appVersion, String appId, boolean trustChainVerification, byte[][] filterChain, boolean enablePlatformCertificatesValidation, String upstreamTlsSni, byte[][] runtimeGuards, long h3ConnectionKeepaliveInitialIntervalMilliseconds, boolean useQuicPlatformPacketWriter, boolean enableQuicConnectionMigration, boolean migrateIdleQuicConnection, diff --git a/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java b/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java index 0d543ce7ab176..f5a367843c746 100644 --- a/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java +++ b/mobile/library/java/org/chromium/net/impl/NativeCronvoyEngineBuilderImpl.java @@ -341,13 +341,14 @@ private EnvoyConfiguration createEnvoyConfiguration() { mDnsRefreshSeconds, mDnsFailureRefreshSecondsBase, mDnsFailureRefreshSecondsMax, mDnsQueryTimeoutSeconds, mDnsMinRefreshSeconds, mDnsPreresolveHostnames, mEnableDNSCache, mDnsCacheSaveIntervalSeconds, mDnsNumRetries.orElse(-1), mEnableDrainPostDnsRefresh, - quicEnabled(), quicConnectionOptions(), quicClientConnectionOptions(), quicHints(), - quicCanonicalSuffixes(), mEnableGzipDecompression, brotliEnabled(), - numTimeoutsToTriggerPortMigration(), mEnableSocketTag, mEnableInterfaceBinding, - mH2ConnectionKeepaliveIdleIntervalMilliseconds, mH2ConnectionKeepaliveTimeoutSeconds, - mMaxConnectionsPerHost, mStreamIdleTimeoutSeconds, mPerTryIdleTimeoutSeconds, mAppVersion, - mAppId, mTrustChainVerification, nativeFilterChain, platformFilterChain, stringAccessors, - keyValueStores, mRuntimeGuards, mEnablePlatformCertificatesValidation, mUpstreamTlsSni, + quicEnabled(), true /* enableEarlyData */, quicConnectionOptions(), + quicClientConnectionOptions(), quicHints(), quicCanonicalSuffixes(), + mEnableGzipDecompression, brotliEnabled(), numTimeoutsToTriggerPortMigration(), + mEnableSocketTag, mEnableInterfaceBinding, mH2ConnectionKeepaliveIdleIntervalMilliseconds, + mH2ConnectionKeepaliveTimeoutSeconds, mMaxConnectionsPerHost, mStreamIdleTimeoutSeconds, + mPerTryIdleTimeoutSeconds, mAppVersion, mAppId, mTrustChainVerification, nativeFilterChain, + platformFilterChain, stringAccessors, keyValueStores, mRuntimeGuards, + mEnablePlatformCertificatesValidation, mUpstreamTlsSni, mH3ConnectionKeepaliveInitialIntervalMilliseconds, mUseQuicPlatformPacketWriter && mUseV2NetworkMonitor, mEnableQuicConnectionMigration && mUseV2NetworkMonitor, mMigrateIdleQuicConnection, diff --git a/mobile/library/jni/BUILD b/mobile/library/jni/BUILD index 78b0d8f9ee1d6..e53c05a2dda6e 100644 --- a/mobile/library/jni/BUILD +++ b/mobile/library/jni/BUILD @@ -77,7 +77,7 @@ envoy_cc_library( ":android_network_utility_lib", ":jni_init_lib", ":jni_utility_lib", - "//library/cc:engine_builder_lib", + "//library/cc:mobile_engine_builder_lib", "//library/common:internal_engine_lib", "//library/common/api:c_types", "//library/common/bridge:utility_lib", diff --git a/mobile/library/jni/jni_impl.cc b/mobile/library/jni/jni_impl.cc index a576b036c8111..7b5ae01e8359e 100644 --- a/mobile/library/jni/jni_impl.cc +++ b/mobile/library/jni/jni_impl.cc @@ -3,7 +3,7 @@ #include "source/common/protobuf/protobuf.h" -#include "library/cc/engine_builder.h" +#include "library/cc/mobile_engine_builder.h" #include "library/common/api/c_types.h" #include "library/common/bridge/utility.h" #include "library/common/extensions/filters/http/platform_bridge/c_types.h" @@ -16,7 +16,7 @@ #include "library/jni/jni_init.h" #include "library/jni/jni_utility.h" -using Envoy::Platform::EngineBuilder; +using Envoy::Platform::MobileEngineBuilder; // NOLINT(namespace-envoy) @@ -107,10 +107,11 @@ extern "C" JNIEXPORT jlong JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibr }; } - return reinterpret_cast( + auto* engine = new Envoy::InternalEngine(std::move(callbacks), std::move(logger), std::move(event_tracker), - /*network_thread_priority*/ absl::nullopt, - (disable_dns_refresh_on_network_change == JNI_TRUE))); + /*network_thread_priority*/ std::nullopt); + engine->disableDnsRefreshOnNetworkChange(disable_dns_refresh_on_network_change == JNI_TRUE); + return reinterpret_cast(engine); } extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibrary_runEngine( @@ -168,6 +169,14 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_dumpStats(JNIEnv* env, return jni_helper.newStringUtf(stats.c_str()).release(); } +extern "C" JNIEXPORT void JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_drainConnectionsBySocketTag(JNIEnv*, jclass, + jlong engine_handle, + jint tag) { + auto engine = reinterpret_cast(engine_handle); + engine->drainConnectionsBySocketTag(static_cast(tag)); +} + // JvmCallbackContext static void passHeaders(const char* method, const Envoy::Types::ManagedEnvoyHeaders& headers, @@ -1066,8 +1075,8 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_registerStringAccessor(JNIEnv* // Takes a jstring from Java, converts it to a C++ string, calls the supplied // setter on it. -void setString(Envoy::JNI::JniHelper& jni_helper, jstring java_string, EngineBuilder* builder, - EngineBuilder& (EngineBuilder::*setter)(std::string)) { +void setString(Envoy::JNI::JniHelper& jni_helper, jstring java_string, MobileEngineBuilder* builder, + MobileEngineBuilder& (MobileEngineBuilder::*setter)(std::string)) { if (!java_string) { return; } @@ -1142,12 +1151,12 @@ void configureBuilder( jlong dns_failure_refresh_seconds_max, jlong dns_query_timeout_seconds, jlong dns_min_refresh_seconds, jobjectArray dns_preresolve_hostnames, jboolean enable_dns_cache, jlong dns_cache_save_interval_seconds, jint dns_num_retries, - jboolean enable_drain_post_dns_refresh, jboolean enable_http3, jstring http3_connection_options, - jstring http3_client_connection_options, jobjectArray quic_hints, - jobjectArray quic_canonical_suffixes, jboolean enable_gzip_decompression, - jboolean enable_brotli_decompression, jint num_timeouts_to_trigger_port_migration, - jboolean enable_socket_tagging, jboolean enable_interface_binding, - jlong h2_connection_keepalive_idle_interval_milliseconds, + jboolean enable_drain_post_dns_refresh, jboolean enable_http3, jboolean enable_early_data, + jstring http3_connection_options, jstring http3_client_connection_options, + jobjectArray quic_hints, jobjectArray quic_canonical_suffixes, + jboolean enable_gzip_decompression, jboolean enable_brotli_decompression, + jint num_timeouts_to_trigger_port_migration, jboolean enable_socket_tagging, + jboolean enable_interface_binding, jlong h2_connection_keepalive_idle_interval_milliseconds, jlong h2_connection_keepalive_timeout_seconds, jlong max_connections_per_host, jlong stream_idle_timeout_seconds, jlong per_try_idle_timeout_seconds, jstring app_version, jstring app_id, jboolean trust_chain_verification, jobjectArray filter_chain, @@ -1155,7 +1164,7 @@ void configureBuilder( jobjectArray runtime_guards, jlong h3_connection_keepalive_initial_interval_milliseconds, jboolean use_quic_platform_packet_writer, jboolean enable_connection_migration, jboolean migrate_idle_connection, jlong max_idle_time_before_migration_seconds, - jlong max_time_on_non_default_network_seconds, Envoy::Platform::EngineBuilder& builder) { + jlong max_time_on_non_default_network_seconds, MobileEngineBuilder& builder) { builder.addConnectTimeoutSeconds((connect_timeout_seconds)); builder.setDisableDnsRefreshOnFailure(disable_dns_refresh_on_failure); builder.setDisableDnsRefreshOnNetworkChange(disable_dns_refresh_on_network_change); @@ -1173,8 +1182,8 @@ void configureBuilder( (h2_connection_keepalive_idle_interval_milliseconds)); builder.addH2ConnectionKeepaliveTimeoutSeconds((h2_connection_keepalive_timeout_seconds)); - setString(jni_helper, app_version, &builder, &EngineBuilder::setAppVersion); - setString(jni_helper, app_id, &builder, &EngineBuilder::setAppId); + setString(jni_helper, app_version, &builder, &MobileEngineBuilder::setAppVersion); + setString(jni_helper, app_id, &builder, &MobileEngineBuilder::setAppId); builder.setDeviceOs("Android"); builder.setStreamIdleTimeoutSeconds((stream_idle_timeout_seconds)); @@ -1183,6 +1192,8 @@ void configureBuilder( builder.enableBrotliDecompression(enable_brotli_decompression == JNI_TRUE); builder.enableSocketTagging(enable_socket_tagging == JNI_TRUE); builder.enableHttp3(enable_http3 == JNI_TRUE); + // TODO(bsoumith): Expose enable_scone to mobile language bindings + builder.enableEarlyData(enable_early_data == JNI_TRUE); builder.setHttp3ConnectionOptions( Envoy::JNI::javaStringToCppString(jni_helper, http3_connection_options)); builder.setHttp3ClientConnectionOptions( @@ -1236,7 +1247,7 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_getNativeFilterConfig(JNIEnv* e jstring filter_name_jstr) { Envoy::JNI::JniHelper jni_helper(env); std::string filter_name = Envoy::JNI::javaStringToCppString(jni_helper, filter_name_jstr); - std::string filter_config = EngineBuilder::nativeNameToConfig(filter_name); + std::string filter_config = MobileEngineBuilder::nativeNameToConfig(filter_name); return jni_helper.newStringUtf(filter_config.c_str()).release(); } @@ -1248,12 +1259,12 @@ extern "C" JNIEXPORT jlong JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibr jlong dns_query_timeout_seconds, jlong dns_min_refresh_seconds, jobjectArray dns_preresolve_hostnames, jboolean enable_dns_cache, jlong dns_cache_save_interval_seconds, jint dns_num_retries, - jboolean enable_drain_post_dns_refresh, jboolean enable_http3, jstring http3_connection_options, - jstring http3_client_connection_options, jobjectArray quic_hints, - jobjectArray quic_canonical_suffixes, jboolean enable_gzip_decompression, - jboolean enable_brotli_decompression, jint num_timeouts_to_trigger_port_migration, - jboolean enable_socket_tagging, jboolean enable_interface_binding, - jlong h2_connection_keepalive_idle_interval_milliseconds, + jboolean enable_drain_post_dns_refresh, jboolean enable_http3, jboolean enable_early_data, + jstring http3_connection_options, jstring http3_client_connection_options, + jobjectArray quic_hints, jobjectArray quic_canonical_suffixes, + jboolean enable_gzip_decompression, jboolean enable_brotli_decompression, + jint num_timeouts_to_trigger_port_migration, jboolean enable_socket_tagging, + jboolean enable_interface_binding, jlong h2_connection_keepalive_idle_interval_milliseconds, jlong h2_connection_keepalive_timeout_seconds, jlong max_connections_per_host, jlong stream_idle_timeout_seconds, jlong per_try_idle_timeout_seconds, jstring app_version, jstring app_id, jboolean trust_chain_verification, jobjectArray filter_chain, @@ -1263,14 +1274,14 @@ extern "C" JNIEXPORT jlong JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibr jboolean migrate_idle_connection, jlong max_idle_time_before_migration_seconds, jlong max_time_on_non_default_network_seconds) { Envoy::JNI::JniHelper jni_helper(env); - Envoy::Platform::EngineBuilder builder; + MobileEngineBuilder builder; configureBuilder( jni_helper, connect_timeout_seconds, disable_dns_refresh_on_failure, disable_dns_refresh_on_network_change, dns_refresh_seconds, dns_failure_refresh_seconds_base, dns_failure_refresh_seconds_max, dns_query_timeout_seconds, dns_min_refresh_seconds, dns_preresolve_hostnames, enable_dns_cache, dns_cache_save_interval_seconds, dns_num_retries, - enable_drain_post_dns_refresh, enable_http3, http3_connection_options, + enable_drain_post_dns_refresh, enable_http3, enable_early_data, http3_connection_options, http3_client_connection_options, quic_hints, quic_canonical_suffixes, enable_gzip_decompression, enable_brotli_decompression, num_timeouts_to_trigger_port_migration, enable_socket_tagging, enable_interface_binding, @@ -1281,7 +1292,7 @@ extern "C" JNIEXPORT jlong JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibr h3_connection_keepalive_initial_interval_milliseconds, use_quic_platform_packet_writer, enable_connection_migration, migrate_idle_connection, max_idle_time_before_migration_seconds, max_time_on_non_default_network_seconds, builder); - return reinterpret_cast(builder.generateBootstrap().release()); + return reinterpret_cast(builder.generateBootstrap().value().release()); } #if defined(__GNUC__) diff --git a/mobile/library/jni/jni_utility.cc b/mobile/library/jni/jni_utility.cc index 41fe3ec4a2cda..61fb28cf6a73a 100644 --- a/mobile/library/jni/jni_utility.cc +++ b/mobile/library/jni/jni_utility.cc @@ -450,7 +450,7 @@ LocalRefUniquePtr protoToJavaByteArray(JniHelper& jni_helper, size_t size = source.ByteSizeLong(); LocalRefUniquePtr byte_array = jni_helper.newByteArray(size); auto bytes = jni_helper.getByteArrayElements(byte_array.get(), nullptr); - source.SerializeToArray(bytes.get(), size); + std::ignore = source.SerializeToArray(bytes.get(), size); return byte_array; } @@ -707,6 +707,9 @@ envoy_stream_intel javaStreamIntelToCppStreamIntel(JniHelper& jni_helper, /* connection_id= */ static_cast(java_connection_id), /* attempt_count= */ static_cast(java_attempt_count), /* consumed_bytes_from_response= */ static_cast(java_consumed_bytes_from_response), + // TODO(bsoumith): Implement SCONE propagation in mobile language bindings + /* scone_max_kbps= */ -1, + /* scone_timestamp_ms= */ -1, }; } @@ -716,6 +719,7 @@ LocalRefUniquePtr cppStreamIntelToJavaStreamIntel(JniHelper& jni_helper jni_helper.findClassFromCache("io/envoyproxy/envoymobile/engine/types/EnvoyStreamIntel"); auto java_stream_intel_init_method_id = jni_helper.getMethodIdFromCache(java_stream_intel_class, "", "(JJJJ)V"); + // TODO(bsoumith): Implement SCONE propagation in mobile language bindings return jni_helper.newObject(java_stream_intel_class, java_stream_intel_init_method_id, static_cast(stream_intel.stream_id), static_cast(stream_intel.connection_id), diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/Engine.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/Engine.kt index 1f75449b16786..78483852b5181 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/Engine.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/Engine.kt @@ -22,6 +22,9 @@ interface Engine { */ fun dumpStats(): String + /** Drains connections matching the given socket tag. */ + fun drainConnectionsBySocketTag(tag: Int) + /** Refresh DNS, and drain connections owned by this Engine. */ fun resetConnectivityState() } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt index 798efca14e63f..64bb2f4c2a9c0 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt @@ -47,6 +47,7 @@ open class EngineBuilder() { private var dnsNumRetries: Int? = null private var enableDrainPostDnsRefresh = false internal var enableHttp3 = true + private var enableEarlyData = true private var http3ConnectionOptions = "" private var http3ClientConnectionOptions = "" private var quicHints = mutableMapOf() @@ -229,6 +230,17 @@ open class EngineBuilder() { return this } + /** + * Specify whether to enable early data (0-RTT) support. Defaults to true. + * + * @param enableEarlyData whether or not to enable early data. + * @return This builder. + */ + fun enableEarlyData(enableEarlyData: Boolean): EngineBuilder { + this.enableEarlyData = enableEarlyData + return this + } + /** * Specify whether to do brotli response decompression or not. Defaults to false. * @@ -550,6 +562,7 @@ open class EngineBuilder() { dnsNumRetries ?: -1, enableDrainPostDnsRefresh, enableHttp3, + enableEarlyData, http3ConnectionOptions, http3ClientConnectionOptions, quicHints, diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt index 50e288f058848..0e393de430508 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/EngineImpl.kt @@ -39,6 +39,10 @@ class EngineImpl( return envoyEngine.dumpStats() } + override fun drainConnectionsBySocketTag(tag: Int) { + envoyEngine.drainConnectionsBySocketTag(tag) + } + override fun resetConnectivityState() { envoyEngine.resetConnectivityState() } diff --git a/mobile/library/objective-c/BUILD b/mobile/library/objective-c/BUILD index 4e732b72972ef..e8c0e3fa4aa77 100644 --- a/mobile/library/objective-c/BUILD +++ b/mobile/library/objective-c/BUILD @@ -54,7 +54,7 @@ envoy_objc_library( ":envoy_key_value_store_bridge_impl_lib", ":envoy_key_value_store_lib", ":envoy_objc_bridge_lib", - "//library/cc:engine_builder_lib", + "//library/cc:mobile_engine_builder_lib", "//library/cc:network_change_monitor_interface", "//library/common:internal_engine_lib", "//library/common/api:c_types", diff --git a/mobile/library/objective-c/EnvoyConfiguration.h b/mobile/library/objective-c/EnvoyConfiguration.h index a22080fba2ea0..bb1041fac0e94 100644 --- a/mobile/library/objective-c/EnvoyConfiguration.h +++ b/mobile/library/objective-c/EnvoyConfiguration.h @@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) UInt32 dnsCacheSaveIntervalSeconds; @property (nonatomic, assign) NSInteger dnsNumRetries; @property (nonatomic, assign) BOOL enableHttp3; +@property (nonatomic, assign) BOOL enableEarlyData; @property (nonatomic, strong) NSDictionary *quicHints; @property (nonatomic, strong) NSArray *quicCanonicalSuffixes; @property (nonatomic, assign) BOOL enableGzipDecompression; @@ -62,6 +63,7 @@ NS_ASSUME_NONNULL_BEGIN dnsCacheSaveIntervalSeconds:(UInt32)dnsCacheSaveIntervalSeconds dnsNumRetries:(NSInteger)dnsNumRetries enableHttp3:(BOOL)enableHttp3 + enableEarlyData:(BOOL)enableEarlyData quicHints:(NSDictionary *)quicHints quicCanonicalSuffixes:(NSArray *)quicCanonicalSuffixes enableGzipDecompression:(BOOL)enableGzipDecompression diff --git a/mobile/library/objective-c/EnvoyConfiguration.mm b/mobile/library/objective-c/EnvoyConfiguration.mm index 7164bc3921163..1e0aceeca2d99 100644 --- a/mobile/library/objective-c/EnvoyConfiguration.mm +++ b/mobile/library/objective-c/EnvoyConfiguration.mm @@ -1,9 +1,11 @@ #import "library/objective-c/EnvoyEngine.h" -#import "library/cc/engine_builder.h" +#import "library/cc/mobile_engine_builder.h" #import "library/cc/direct_response_testing.h" #include "source/common/protobuf/utility.h" +using Envoy::Platform::MobileEngineBuilder; + @implementation NSString (CXX) - (std::string)toCXXString { return std::string([self UTF8String], @@ -78,6 +80,7 @@ - (instancetype)initWithConnectTimeoutSeconds:(UInt32)connectTimeoutSeconds dnsCacheSaveIntervalSeconds:(UInt32)dnsCacheSaveIntervalSeconds dnsNumRetries:(NSInteger)dnsNumRetries enableHttp3:(BOOL)enableHttp3 + enableEarlyData:(BOOL)enableEarlyData quicHints:(NSDictionary *)quicHints quicCanonicalSuffixes:(NSArray *)quicCanonicalSuffixes enableGzipDecompression:(BOOL)enableGzipDecompression @@ -124,6 +127,7 @@ - (instancetype)initWithConnectTimeoutSeconds:(UInt32)connectTimeoutSeconds self.dnsCacheSaveIntervalSeconds = dnsCacheSaveIntervalSeconds; self.dnsNumRetries = dnsNumRetries; self.enableHttp3 = enableHttp3; + self.enableEarlyData = enableEarlyData; self.quicHints = quicHints; self.quicCanonicalSuffixes = quicCanonicalSuffixes; self.enableGzipDecompression = enableGzipDecompression; @@ -152,8 +156,8 @@ - (instancetype)initWithConnectTimeoutSeconds:(UInt32)connectTimeoutSeconds return self; } -- (Envoy::Platform::EngineBuilder)applyToCXXBuilder { - Envoy::Platform::EngineBuilder builder; +- (MobileEngineBuilder)applyToCXXBuilder { + MobileEngineBuilder builder; for (EnvoyNativeFilterConfig *nativeFilterConfig in [self.nativeFilterChain reverseObjectEnumerator]) { @@ -167,6 +171,7 @@ - (instancetype)initWithConnectTimeoutSeconds:(UInt32)connectTimeoutSeconds } builder.enableHttp3(self.enableHttp3); + builder.enableEarlyData(self.enableEarlyData); for (NSString *host in self.quicHints) { builder.addQuicHint([host toCXXString], [[self.quicHints objectForKey:host] intValue]); } @@ -225,8 +230,8 @@ - (instancetype)initWithConnectTimeoutSeconds:(UInt32)connectTimeoutSeconds - (std::unique_ptr)generateBootstrap { try { - Envoy::Platform::EngineBuilder builder = [self applyToCXXBuilder]; - return builder.generateBootstrap(); + MobileEngineBuilder builder = [self applyToCXXBuilder]; + return std::move(builder.generateBootstrap().value()); } catch (const std::exception &e) { NSLog(@"[Envoy] error generating bootstrap: %@", @(e.what())); return nullptr; diff --git a/mobile/library/objective-c/EnvoyEngine.h b/mobile/library/objective-c/EnvoyEngine.h index 280848332fba6..65a43e37990d9 100644 --- a/mobile/library/objective-c/EnvoyEngine.h +++ b/mobile/library/objective-c/EnvoyEngine.h @@ -68,6 +68,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSString *)dumpStats; +- (void)drainConnectionsBySocketTag:(uint32_t)tag; + - (void)terminate; - (void)resetConnectivityState; diff --git a/mobile/library/objective-c/EnvoyEngineImpl.mm b/mobile/library/objective-c/EnvoyEngineImpl.mm index 330ac574e276e..fa94ae15a48ef 100644 --- a/mobile/library/objective-c/EnvoyEngineImpl.mm +++ b/mobile/library/objective-c/EnvoyEngineImpl.mm @@ -8,7 +8,7 @@ #import "library/common/types/c_types.h" #import "library/common/extensions/key_value/platform/c_types.h" -#import "library/cc/engine_builder.h" +#import "library/cc/mobile_engine_builder.h" #import "library/cc/network_change_monitor.h" #import "library/common/internal_engine.h" #include "library/common/system/system_helper.h" @@ -578,6 +578,10 @@ - (NSString *)dumpStats { return @(status.c_str()); } +- (void)drainConnectionsBySocketTag:(uint32_t)tag { + _engine->drainConnectionsBySocketTag(tag); +} + - (void)terminate { _engine->terminate(); } diff --git a/mobile/library/python/BUILD b/mobile/library/python/BUILD index bea61dd4cdf46..cb4a6d4cb36c1 100644 --- a/mobile/library/python/BUILD +++ b/mobile/library/python/BUILD @@ -1,12 +1,13 @@ load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") +load("@mobile_pip3//:requirements.bzl", "requirement") load("@pybind11_bazel//:build_defs.bzl", "pybind_extension", "pybind_library") -load("@python_abi//:abi.bzl", "abi_tag", "python_tag") load("@rules_python//python:defs.bzl", "py_library") load("@rules_python//python:packaging.bzl", "py_package", "py_wheel") licenses(["notice"]) # Apache 2 -package(default_visibility = ["//visibility:public"]) +envoy_mobile_package() string_flag( name = "python_platform", @@ -28,6 +29,26 @@ config_setting( flag_values = {":python_platform": "macosx_11_0_arm64"}, ) +string_flag( + name = "python_version", + build_setting_default = "3.12", +) + +config_setting( + name = "is_python_3_12", + flag_values = {":python_version": "3.12"}, +) + +config_setting( + name = "is_python_3_13", + flag_values = {":python_version": "3.13"}, +) + +config_setting( + name = "is_python_3_14", + flag_values = {":python_version": "3.14"}, +) + pybind_library( name = "envoy_engine_lib", srcs = [ @@ -93,7 +114,10 @@ py_library( ], imports = ["."], visibility = ["//visibility:public"], - deps = [":async_client"], + deps = [ + ":async_client", + requirement("httpx"), + ], ) py_package( @@ -107,7 +131,11 @@ py_package( # --//library/python:python_platform="manylinux2014_x86_64" \ py_wheel( name = "envoy_mobile_wheel", - abi = abi_tag(), + abi = select({ + ":is_python_3_14": "cp314", + ":is_python_3_13": "cp313", + "//conditions:default": "cp312", + }), distribution = "envoy-mobile-client", platform = select({ ":is_linux_x86_64": "manylinux2014_x86_64", @@ -115,7 +143,11 @@ py_wheel( ":is_macos_arm64": "macosx_11_0_arm64", "//conditions:default": "any", }), - python_tag = python_tag(), + python_tag = select({ + ":is_python_3_14": "cp314", + ":is_python_3_13": "cp313", + "//conditions:default": "cp312", + }), strip_path_prefixes = [ "mobile/library/python", "library/python", diff --git a/mobile/library/python/envoy_mobile/__init__.py b/mobile/library/python/envoy_mobile/__init__.py index eb3b1e4321756..1852ce03a19c1 100644 --- a/mobile/library/python/envoy_mobile/__init__.py +++ b/mobile/library/python/envoy_mobile/__init__.py @@ -1,4 +1,3 @@ -from .async_client.client import AsyncClient from .envoy_engine import ( Engine, EngineBuilder, @@ -11,9 +10,16 @@ StreamIntel, StreamPrototype, ) +from .async_client.client import AsyncClient +from .async_client_transport import AsyncEnvoyClientTransport +from .sync_client_transport import EnvoyClientTransport +from .transport_factory import EnvoyTransportFactory __all__ = [ "AsyncClient", + "AsyncEnvoyClientTransport", + "EnvoyClientTransport", + "EnvoyTransportFactory", "Engine", "EngineBuilder", "EnvoyError", diff --git a/mobile/library/python/envoy_mobile/async_client/client.py b/mobile/library/python/envoy_mobile/async_client/client.py index c5dfc347a9566..adb7772f22d58 100644 --- a/mobile/library/python/envoy_mobile/async_client/client.py +++ b/mobile/library/python/envoy_mobile/async_client/client.py @@ -1,10 +1,9 @@ """High level asyncio client built on top of the Envoy Mobile bindings.""" import asyncio -from typing import Any, Dict, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Protocol, Union from .. import envoy_engine -from ..envoy_engine import EngineBuilder from .executor import AsyncioExecutor, Executor from .response import ClientResponseError, Response @@ -13,6 +12,16 @@ ) +class EngineBuilderLike(Protocol): + def set_on_engine_running(self, closure: Callable[[], None]) -> "EngineBuilderLike": + """Set callback to be invoked when engine is running.""" + ... + + def build(self) -> Any: + """Build and return the engine instance.""" + ... + + class AsyncClient: """A very small HTTP client that speaks the subset of envoy_requests that we care about. @@ -25,13 +34,15 @@ class AsyncClient: Use as an async context manager: ``async with AsyncClient(engine_builder) as client:`` """ - def __init__(self, engine_builder: EngineBuilder) -> None: + def __init__(self, engine_builder: EngineBuilderLike, listener_name: str = "") -> None: """Construct a new AsyncClient. Args: - engine_builder: A pre-configured EngineBuilder to finalize and build. + engine_builder: A pre-configured EngineBuilder (or compatible builder object) to finalize and build. + listener_name: The name of the API listener to route streams to. """ self._engine_builder = engine_builder + self._listener_name = listener_name self._engine = None self._executor: Optional[Executor] = None self._engine_running = None @@ -70,7 +81,7 @@ async def request(self, method: str, url: str, **kwargs) -> Response: stream_complete = asyncio.Event() header_complete = asyncio.Event() response = Response(header_complete, stream_complete, self._executor) - stream = response.attach(self._engine) + stream = response.attach(self._engine, self._listener_name) self._send_request(stream, method, url, **kwargs) # Wait for either the response headers to arrive or an error to occur. await asyncio.wait( diff --git a/mobile/library/python/envoy_mobile/async_client/executor.py b/mobile/library/python/envoy_mobile/async_client/executor.py index 83f32aaac53d6..7ae98cefe7c06 100644 --- a/mobile/library/python/envoy_mobile/async_client/executor.py +++ b/mobile/library/python/envoy_mobile/async_client/executor.py @@ -17,18 +17,39 @@ def wrap(self, fn: Func) -> Func: class AsyncioExecutor(Executor): + """Executor that schedules callbacks on an asyncio event loop.""" + def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None: # Allow an explicit loop to be supplied (useful for testing). # If none is provided, attempt to grab the currently running loop. if loop is None: - loop = asyncio.get_running_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + # Fallback for when no loop is running in the current thread. + # In such cases, the executor must be explicitly initialized + # with the correct loop. + pass self.loop = loop def wrap(self, fn: Func) -> Func: + """Return a version of ``fn`` that is safe to call from any thread.""" + @functools.wraps(fn) - def wrapper(*args: Any, **kwargs: Any) -> None: # type: ignore[return-value] + def wrapper(*args: Any, **kwargs: Any) -> Any: + if self.loop is None: + # If we don't have a loop, we can't schedule the callback. + # In a real-world scenario, this might happen if the loop + # was closed or if it was never correctly initialized. + raise RuntimeError("AsyncioExecutor must be initialized with a running loop.") + + if self.loop.is_closed(): + return None + # ``call_soon_threadsafe`` will schedule the callable on the same - # loop that constructed the executor, making every callback "safe" + # loop that constructed the executor, making every callback "safe". + # Note: call_soon_threadsafe doesn't return the result of fn. self.loop.call_soon_threadsafe(fn, *args, **kwargs) + return None return cast(Func, wrapper) diff --git a/mobile/library/python/envoy_mobile/async_client/response.py b/mobile/library/python/envoy_mobile/async_client/response.py index 391e57a80eb9a..bcf8703a2c631 100644 --- a/mobile/library/python/envoy_mobile/async_client/response.py +++ b/mobile/library/python/envoy_mobile/async_client/response.py @@ -44,8 +44,8 @@ def __init__( self.__streaming_started = False self.__stream_reader: Optional["StreamReader"] = None - def attach(self, engine: envoy_engine.Engine) -> envoy_engine.Stream: - proto = engine.stream_client().new_stream_prototype() + def attach(self, engine: envoy_engine.Engine, listener_name: str = "") -> envoy_engine.Stream: + proto = engine.stream_client(listener_name).new_stream_prototype() self.__stream = proto.start( on_headers=self.__executor.wrap(self.on_headers), on_data=self.__executor.wrap(self.on_data), diff --git a/mobile/library/python/envoy_mobile/async_client_transport.py b/mobile/library/python/envoy_mobile/async_client_transport.py new file mode 100644 index 0000000000000..e1ce8467472b7 --- /dev/null +++ b/mobile/library/python/envoy_mobile/async_client_transport.py @@ -0,0 +1,216 @@ +"""Async httpx transport for Envoy Mobile.""" + +import asyncio +from typing import AsyncIterable, Dict, List, Optional, Union + +import httpx +from . import envoy_engine +from .async_client.executor import AsyncioExecutor +from .httpx_utils import get_envoy_headers, map_envoy_error, get_next_socket_tag + + +class AsyncEnvoyStream(httpx.AsyncByteStream): + """An asynchronous byte stream that reads data from an Envoy stream.""" + + def __init__( + self, + stream: envoy_engine.Stream, + queue: asyncio.Queue, + stream_complete: asyncio.Event, + executor: AsyncioExecutor, + ) -> None: + self._stream = stream + self._queue = queue + self._stream_complete = stream_complete + self._executor = executor + self._closed = False + + async def __aiter__(self) -> AsyncIterable[bytes]: + try: + while True: + # Use explicit flow control to request more data from Envoy. + # We request a chunk and then wait for the queue to populate. + if not self._stream_complete.is_set(): + # Request up to 64KB at a time + self._stream.read_data(65536) + + # Wait for data or completion + item = await self._queue.get() + if item is None: # EOF + break + if isinstance(item, Exception): + raise item + + yield item + finally: + await self.aclose() + + async def aclose(self) -> None: + if not self._closed: + if not self._stream_complete.is_set(): + self._stream.cancel() + self._closed = True + + +class AsyncResponseHandler: + """Handles callbacks from the Envoy engine and pushes data to asyncio queues.""" + + def __init__(self, executor: AsyncioExecutor) -> None: + self.executor = executor + self.headers_future: asyncio.Future = asyncio.Future() + self.data_queue: asyncio.Queue = asyncio.Queue() + self.stream_complete = asyncio.Event() + self.status_code: Optional[int] = None + self.headers: List[tuple] = [] + self.trailers: Dict[str, Union[str, List[str]]] = {} + + def on_headers( + self, + headers: Dict[str, Union[str, List[str]]], + end_stream: bool, + intel: envoy_engine.StreamIntel, + ) -> None: + status = headers.get(":status") + if status is not None: + try: + self.status_code = int(status[0] if isinstance(status, list) else status) + except (ValueError, IndexError): + pass + + for key, value in headers.items(): + if not key.startswith(":"): + if isinstance(value, list): + for val in value: + self.headers.append((key, val)) + else: + self.headers.append((key, value)) + + if not self.headers_future.done(): + self.headers_future.set_result(True) + + if end_stream: + self.data_queue.put_nowait(None) + self.stream_complete.set() + + def on_data( + self, + data: bytes, + length: int, + end_stream: bool, + intel: envoy_engine.StreamIntel, + ) -> None: + self.data_queue.put_nowait(data) + if end_stream: + self.data_queue.put_nowait(None) + + def on_trailers( + self, + trailers: Dict[str, Union[str, List[str]]], + intel: envoy_engine.StreamIntel, + ) -> None: + for key, value in trailers.items(): + self.trailers[key] = value[0] if isinstance(value, list) and len(value) == 1 else value + self.data_queue.put_nowait(None) + + def on_complete( + self, intel: envoy_engine.StreamIntel, final_intel: envoy_engine.FinalStreamIntel + ) -> None: + if not self.stream_complete.is_set(): + self.data_queue.put_nowait(None) + self.stream_complete.set() + + def on_error( + self, + error: envoy_engine.EnvoyError, + intel: envoy_engine.StreamIntel, + final_intel: envoy_engine.FinalStreamIntel, + ) -> None: + exc = map_envoy_error(error.error_code, error.message) + if not self.headers_future.done(): + self.headers_future.set_exception(exc) + self.data_queue.put_nowait(exc) + self.stream_complete.set() + + def on_cancel( + self, intel: envoy_engine.StreamIntel, final_intel: envoy_engine.FinalStreamIntel + ) -> None: + exc = httpx.RequestError("Request cancelled") + if not self.headers_future.done(): + self.headers_future.set_exception(exc) + self.data_queue.put_nowait(exc) + self.stream_complete.set() + + +class AsyncEnvoyClientTransport(httpx.AsyncBaseTransport): + """An asynchronous transport for httpx that uses Envoy Mobile.""" + + def __init__(self, engine: envoy_engine.Engine, listener_name: str = "") -> None: + self._engine = engine + self._listener_name = listener_name + self._executor = AsyncioExecutor() + self._transport_tag = get_next_socket_tag() + + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + # Map headers + timeout = request.extensions.get("timeout", {}).get("read") + envoy_headers = get_envoy_headers(request, timeout=timeout, socket_tag=self._transport_tag) + + # Create handler + handler = AsyncResponseHandler(self._executor) + + # Start stream + proto = self._engine.stream_client(self._listener_name).new_stream_prototype() + stream = proto.start( + on_headers=self._executor.wrap(handler.on_headers), + on_data=self._executor.wrap(handler.on_data), + on_trailers=self._executor.wrap(handler.on_trailers), + on_complete=self._executor.wrap(handler.on_complete), + on_error=self._executor.wrap(handler.on_error), + on_cancel=self._executor.wrap(handler.on_cancel), + explicit_flow_control=True, + ) + + # --- Send Request --- + # + # In httpx, the request body is accessed via `request.stream`, which provides + # an asynchronous iterator over the body chunks. This is crucial for: + # 1. Memory Efficiency: We don't load the entire body into memory, which + # is essential for large file uploads. + # 2. Support for Generators: If the user provides a generator as the + # request content, we consume it one chunk at a time. + # + # Simplified Request Logic: + # 1. Always send headers first with `end_stream=False`. + # 2. Stream all chunks from `request.stream` with `end_stream=False`. + # 3. Finalize by sending an empty string with `end_stream=True` (via `stream.close(b"")`). + + # Start by sending the request headers. + # Note: These stream operations are synchronous in Python because they are + # non-blocking calls that interface directly with the Envoy Mobile C++ engine. + stream.send_headers(envoy_headers, False) + + # Iterate through the request stream and send all data chunks. + async for chunk in request.stream: + stream.send_data(chunk) + + # Finalize the request. Sending an empty string with `stream.close()` + # signals `end_stream=True` to Envoy, completing the request side of the stream. + stream.close(b"") + + # Wait for headers + try: + await handler.headers_future + except Exception: + stream.cancel() + raise + + return httpx.Response( + status_code=handler.status_code or 0, + headers=handler.headers, + stream=AsyncEnvoyStream( + stream, handler.data_queue, handler.stream_complete, self._executor + ), + ) + + async def aclose(self) -> None: + self._engine.drain_connections_by_socket_tag(self._transport_tag) diff --git a/mobile/library/python/envoy_mobile/httpx_utils.py b/mobile/library/python/envoy_mobile/httpx_utils.py new file mode 100644 index 0000000000000..1df492424110d --- /dev/null +++ b/mobile/library/python/envoy_mobile/httpx_utils.py @@ -0,0 +1,96 @@ +"""Utilities for mapping httpx requests and responses to Envoy Mobile.""" + +import itertools +from typing import Dict, List, Optional, Union +from urllib.parse import urlparse +import httpx +from .async_client.utils import ( + normalize_method, + normalize_headers, + normalize_timeout_to_ms, +) + +_tag_generator = itertools.count(1) + + +def get_next_socket_tag() -> int: + """Generate a unique 32-bit socket tag for connection pool isolation.""" + return next(_tag_generator) & 0xFFFFFFFF + + +def get_envoy_headers( + request: httpx.Request, + timeout: Optional[Union[int, float]] = None, + socket_tag: Optional[int] = None, +) -> Dict[str, Union[str, List[str]]]: + """Map an httpx.Request to Envoy-compatible headers. + + Args: + request: The httpx.Request object. + timeout: Optional request timeout in seconds. + + Returns: + A dictionary of Envoy-compatible headers, including pseudo-headers. + """ + method = normalize_method(request.method) + url = str(request.url) + parsed = urlparse(url) + + # Convert httpx.Headers to the dict format expected by normalize_headers + # httpx.Headers.items() provides (key, value) pairs. + headers_dict: Dict[str, str] = dict(request.headers.items()) + norm_headers = normalize_headers(headers_dict) + + header_dict: Dict[str, Union[str, List[str]]] = { + ":method": method, + ":scheme": request.url.scheme, + ":authority": request.url.netloc.decode("ascii"), + ":path": request.url.raw_path.decode("ascii"), + } + + # Add timeout header if specified + timeout_ms = normalize_timeout_to_ms(timeout) + if timeout_ms > 0: + header_dict["x-envoy-upstream-rq-timeout-ms"] = str(timeout_ms) + + # Add normalized user headers + for key, values in norm_headers.items(): + # Avoid overriding pseudo-headers and avoid duplicate host headers. + # Envoy Mobile automatically sets the correct upstream authority/host headers. + if key.startswith(":") or key.lower() == "host": + continue + # Avoid HTTP/1.1 connection-specific headers, which are forbidden in HTTP/2. + # Envoy Mobile will handle and set corresponding upstream connection headers properly on its own. + if key.lower() in { + "connection", + "keep-alive", + "proxy-connection", + "transfer-encoding", + "upgrade", + }: + continue + header_dict[key] = values if len(values) > 1 else values[0] + + # Add socket tag header if specified + if socket_tag is not None: + header_dict["x-envoy-mobile-socket-tag"] = f"0,{socket_tag}" + + return header_dict + + +def map_envoy_error(error_code: int, message: str) -> Exception: + """Map an Envoy error code to an appropriate httpx exception.""" + # Envoy error codes are defined in library/python/module_definition.cc + # ENVOY_STREAM_RESET = 1 + # ENVOY_CONNECTION_FAILURE = 2 + # ENVOY_BUFFER_LIMIT_EXCEEDED = 3 + # ENVOY_REQUEST_TIMEOUT = 4 + + if error_code == 2: # ConnectionFailure + return httpx.ConnectError(message) + elif error_code == 4: # RequestTimeout + return httpx.ReadTimeout(message) + elif error_code == 1: # StreamReset + return httpx.RemoteProtocolError(message) + + return httpx.RequestError(message) diff --git a/mobile/library/python/envoy_mobile/sync_client_transport.py b/mobile/library/python/envoy_mobile/sync_client_transport.py new file mode 100644 index 0000000000000..9a4e1493af82d --- /dev/null +++ b/mobile/library/python/envoy_mobile/sync_client_transport.py @@ -0,0 +1,202 @@ +"""Synchronous httpx transport for Envoy Mobile.""" + +import queue +import threading +from typing import Dict, Iterable, List, Optional, Union + +import httpx +from . import envoy_engine +from .httpx_utils import get_envoy_headers, map_envoy_error, get_next_socket_tag + + +class SyncEnvoyStream(httpx.SyncByteStream): + def __init__( + self, + stream: envoy_engine.Stream, + data_queue: queue.Queue, + stream_complete: threading.Event, + ) -> None: + self._stream = stream + self._queue = data_queue + self._stream_complete = stream_complete + self._closed = False + + def __iter__(self) -> Iterable[bytes]: + try: + while True: + # Use explicit flow control to request more data from Envoy. + if not self._stream_complete.is_set(): + # Request up to 64KB at a time + self._stream.read_data(65536) + + # Wait for data or completion + # Blocking wait for data. Terminal states push None or Exception to the queue. + item = self._queue.get() + + if item is None: # EOF + break + if isinstance(item, Exception): + raise item + + yield item + finally: + self.close() + + def close(self) -> None: + if not self._closed: + if not self._stream_complete.is_set(): + self._stream.cancel() + self._closed = True + + +class SyncResponseHandler: + def __init__(self) -> None: + self.headers_event = threading.Event() + self.data_queue: queue.Queue = queue.Queue() + self.stream_complete = threading.Event() + self.status_code: Optional[int] = None + self.headers: List[tuple] = [] + self.trailers: Dict[str, Union[str, List[str]]] = {} + self.exception: Optional[Exception] = None + + def on_headers( + self, + headers: Dict[str, Union[str, List[str]]], + end_stream: bool, + intel: envoy_engine.StreamIntel, + ) -> None: + status = headers.get(":status") + if status is not None: + try: + self.status_code = int(status[0] if isinstance(status, list) else status) + except (ValueError, IndexError): + pass + + for key, value in headers.items(): + if not key.startswith(":"): + if isinstance(value, list): + for val in value: + self.headers.append((key, val)) + else: + self.headers.append((key, value)) + + self.headers_event.set() + + if end_stream: + self.data_queue.put(None) + self.stream_complete.set() + + def on_data( + self, + data: bytes, + length: int, + end_stream: bool, + intel: envoy_engine.StreamIntel, + ) -> None: + self.data_queue.put(data) + if end_stream: + self.data_queue.put(None) + + def on_trailers( + self, + trailers: Dict[str, Union[str, List[str]]], + intel: envoy_engine.StreamIntel, + ) -> None: + for key, value in trailers.items(): + self.trailers[key] = value[0] if isinstance(value, list) and len(value) == 1 else value + self.data_queue.put(None) + + def on_complete( + self, intel: envoy_engine.StreamIntel, final_intel: envoy_engine.FinalStreamIntel + ) -> None: + if not self.stream_complete.is_set(): + self.data_queue.put(None) + self.stream_complete.set() + + def on_error( + self, + error: envoy_engine.EnvoyError, + intel: envoy_engine.StreamIntel, + final_intel: envoy_engine.FinalStreamIntel, + ) -> None: + exc = map_envoy_error(error.error_code, error.message) + self.exception = exc + self.data_queue.put(exc) + self.headers_event.set() + self.stream_complete.set() + + def on_cancel( + self, intel: envoy_engine.StreamIntel, final_intel: envoy_engine.FinalStreamIntel + ) -> None: + exc = httpx.RequestError("Request cancelled") + self.exception = exc + self.data_queue.put(exc) + self.headers_event.set() + self.stream_complete.set() + + +class EnvoyClientTransport(httpx.BaseTransport): + def __init__(self, engine: envoy_engine.Engine, listener_name: str = "") -> None: + self._engine = engine + self._listener_name = listener_name + self._transport_tag = get_next_socket_tag() + + def handle_request(self, request: httpx.Request) -> httpx.Response: + # Map headers + timeout = request.extensions.get("timeout", {}).get("read") + envoy_headers = get_envoy_headers(request, timeout=timeout, socket_tag=self._transport_tag) + + # Create handler + handler = SyncResponseHandler() + + # Start stream + proto = self._engine.stream_client(self._listener_name).new_stream_prototype() + stream = proto.start( + on_headers=handler.on_headers, + on_data=handler.on_data, + on_trailers=handler.on_trailers, + on_complete=handler.on_complete, + on_error=handler.on_error, + on_cancel=handler.on_cancel, + explicit_flow_control=True, + ) + + # --- Send Request --- + # + # In httpx, the request body is accessed via `request.stream`, which provides + # an iterator over the body chunks. This is crucial for: + # 1. Memory Efficiency: We don't load the entire body into memory, which + # is essential for large file uploads. + # 2. Support for Generators: If the user provides a generator as the + # request content, we consume it one chunk at a time. + # + # Simplified Request Logic: + # 1. Always send headers first with `end_stream=False`. + # 2. Stream all chunks from `request.stream` with `end_stream=False`. + # 3. Finalize by sending an empty string with `end_stream=True` (via `stream.close(b"")`). + + # Start by sending the request headers. + stream.send_headers(envoy_headers, False) + + # Iterate through the request stream and send all data chunks. + for chunk in request.stream: + stream.send_data(chunk) + + # Finalize the request. Sending an empty string with `stream.close()` + # signals `end_stream=True` to Envoy, completing the request side of the stream. + stream.close(b"") + + # Wait for headers + handler.headers_event.wait() + if handler.exception: + stream.cancel() + raise handler.exception + + return httpx.Response( + status_code=handler.status_code or 0, + headers=handler.headers, + stream=SyncEnvoyStream(stream, handler.data_queue, handler.stream_complete), + ) + + def close(self) -> None: + self._engine.drain_connections_by_socket_tag(self._transport_tag) diff --git a/mobile/library/python/envoy_mobile/transport_factory.py b/mobile/library/python/envoy_mobile/transport_factory.py new file mode 100644 index 0000000000000..b891451c0cdb5 --- /dev/null +++ b/mobile/library/python/envoy_mobile/transport_factory.py @@ -0,0 +1,87 @@ +"""Factory for creating Envoy Mobile httpx transports with a shared engine.""" + +import threading +from typing import Optional + +from . import envoy_engine +from .async_client_transport import AsyncEnvoyClientTransport +from .sync_client_transport import EnvoyClientTransport + + +class EnvoyTransportFactory: + """Factory for managing a single, shared Envoy Engine. + + Envoy Engines are expensive to initialize and should typically live for the + entire life of the process. This factory ensures that only one Engine is + created and shared across all httpx transports. + """ + + _engine: Optional[envoy_engine.Engine] = None + _lock = threading.Lock() + + @classmethod + def get_shared_engine( + cls, builder: Optional[envoy_engine.EngineBuilder] = None + ) -> envoy_engine.Engine: + """Get the shared Envoy Engine instance, initializing it if necessary. + + Args: + builder: An optional EngineBuilder to configure the engine before + it is started. This is only used if the engine hasn't been + initialized yet. + + Returns: + The shared envoy_engine.Engine instance. + """ + if cls._engine is not None: + return cls._engine + + with cls._lock: + if cls._engine is None: + # Use default builder if none provided + if builder is None: + builder = envoy_engine.EngineBuilder() + + # Wait for the engine to be fully running before returning. + # This ensures DNS and other subsystems are initialized. + running_event = threading.Event() + builder.set_on_engine_running(running_event.set) + + # Build and start the engine + cls._engine = builder.build() + + # Timeout after 10 seconds to avoid infinite hang if it fails. + if not running_event.wait(timeout=10.0): + raise RuntimeError("Envoy Mobile engine failed to start within 10 seconds") + + return cls._engine + + @classmethod + def get_async_transport( + cls, builder: Optional[envoy_engine.EngineBuilder] = None, listener_name: str = "" + ) -> AsyncEnvoyClientTransport: + """Create an AsyncEnvoyClientTransport using the shared engine. + + Args: + builder: Optional EngineBuilder for engine initialization. + + Returns: + An AsyncEnvoyClientTransport instance. + """ + engine = cls.get_shared_engine(builder) + return AsyncEnvoyClientTransport(engine, listener_name) + + @classmethod + def get_sync_transport( + cls, builder: Optional[envoy_engine.EngineBuilder] = None, listener_name: str = "" + ) -> EnvoyClientTransport: + """Create an EnvoyClientTransport using the shared engine. + + Args: + builder: Optional EngineBuilder for engine initialization. + + Returns: + An EnvoyClientTransport instance. + """ + engine = cls.get_shared_engine(builder) + return EnvoyClientTransport(engine, listener_name) diff --git a/mobile/library/python/module_definition.cc b/mobile/library/python/module_definition.cc index 575067524b6a7..9bdd07f488c7b 100644 --- a/mobile/library/python/module_definition.cc +++ b/mobile/library/python/module_definition.cc @@ -93,7 +93,7 @@ PYBIND11_MODULE(envoy_engine, m) { }, [](Envoy::EnvoyError& e, py::object val) { if (val.is_none()) { - e.attempt_count_ = absl::nullopt; + e.attempt_count_ = std::nullopt; } else { e.attempt_count_ = val.cast(); } @@ -164,10 +164,12 @@ PYBIND11_MODULE(envoy_engine, m) { // -- Engine -- py::class_(m, "Engine") - .def("stream_client", &Envoy::Platform::Engine::streamClient) + .def("stream_client", &Envoy::Platform::Engine::streamClient, py::arg("listener_name") = "") .def("terminate", &Envoy::Platform::Engine::terminate, py::call_guard()) .def("dump_stats", &Envoy::Platform::Engine::dumpStats, + py::call_guard()) + .def("drain_connections_by_socket_tag", &Envoy::Platform::Engine::drainConnectionsBySocketTag, py::call_guard()); // -- EngineBuilder -- @@ -378,6 +380,12 @@ PYBIND11_MODULE(envoy_engine, m) { return self.enableStatsCollection(on); }, py::arg("stats_collection_on"), py::return_value_policy::reference) + .def( + "enable_worker_thread", + [](Envoy::Platform::EngineBuilder& self, bool on) -> Envoy::Platform::EngineBuilder& { + return self.enableWorkerThread(on); + }, + py::arg("use_worker_thread_on"), py::return_value_policy::reference) .def("build", &Envoy::Platform::EngineBuilder::build, py::call_guard()); } diff --git a/mobile/library/swift/Engine.swift b/mobile/library/swift/Engine.swift index ccd682a9d1232..44ed8feb594f7 100644 --- a/mobile/library/swift/Engine.swift +++ b/mobile/library/swift/Engine.swift @@ -12,6 +12,9 @@ public protocol Engine: AnyObject { func dumpStats() -> String + /// Drains connections matching the given socket tag. + func drainConnectionsBySocketTag(_ tag: UInt32) + /// Terminates the running engine. func terminate() diff --git a/mobile/library/swift/EngineBuilder.swift b/mobile/library/swift/EngineBuilder.swift index a6e98c3461de7..94c4f3d80443e 100644 --- a/mobile/library/swift/EngineBuilder.swift +++ b/mobile/library/swift/EngineBuilder.swift @@ -19,6 +19,7 @@ open class EngineBuilder: NSObject { private var enableGzipDecompression: Bool = true private var enableBrotliDecompression: Bool = false private var enableHttp3: Bool = true + private var enableEarlyData: Bool = true private var quicHints: [String: Int] = [:] private var quicCanonicalSuffixes: [String] = [] private var enableInterfaceBinding: Bool = false @@ -191,6 +192,17 @@ open class EngineBuilder: NSObject { return self } + /// Specify whether to enable early data (0-RTT) support. Defaults to true. + /// + /// - parameter enableEarlyData: whether or not to enable early data. + /// + /// - returns: This builder. + @discardableResult + public func enableEarlyData(_ enableEarlyData: Bool) -> Self { + self.enableEarlyData = enableEarlyData + return self + } + /// Add a host port pair that's known to support QUIC. /// /// - parameter host: the string representation of the host name @@ -541,6 +553,7 @@ open class EngineBuilder: NSObject { dnsCacheSaveIntervalSeconds: self.dnsCacheSaveIntervalSeconds, dnsNumRetries: self.dnsNumRetries, enableHttp3: self.enableHttp3, + enableEarlyData: self.enableEarlyData, quicHints: self.quicHints.mapValues { NSNumber(value: $0) }, quicCanonicalSuffixes: self.quicCanonicalSuffixes, enableGzipDecompression: self.enableGzipDecompression, diff --git a/mobile/library/swift/EngineImpl.swift b/mobile/library/swift/EngineImpl.swift index a91a7d95f513a..80736dc7f48f5 100644 --- a/mobile/library/swift/EngineImpl.swift +++ b/mobile/library/swift/EngineImpl.swift @@ -36,6 +36,10 @@ extension EngineImpl: Engine { self.engine.dumpStats() } + func drainConnectionsBySocketTag(_ tag: UInt32) { + self.engine.drainConnections(bySocketTag: tag) + } + func terminate() { self.engine.terminate() } diff --git a/mobile/library/swift/mocks/MockEnvoyEngine.swift b/mobile/library/swift/mocks/MockEnvoyEngine.swift index d11c91ece1ab4..93509e65255a7 100644 --- a/mobile/library/swift/mocks/MockEnvoyEngine.swift +++ b/mobile/library/swift/mocks/MockEnvoyEngine.swift @@ -39,6 +39,8 @@ extension MockEnvoyEngine: EnvoyEngine { return "" } + func drainConnections(bySocketTag tag: UInt32) {} + func terminate() {} func resetConnectivityState() {} diff --git a/mobile/test/cc/BUILD b/mobile/test/cc/BUILD index a962d0cb570e4..2691119395af7 100644 --- a/mobile/test/cc/BUILD +++ b/mobile/test/cc/BUILD @@ -1,16 +1,36 @@ -load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_mobile_package") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_library", "envoy_mobile_package") +load("//bazel:envoy_mobile_cc_test.bzl", "envoy_cc_test_with_engine_builder") licenses(["notice"]) # Apache 2 envoy_mobile_package() -envoy_cc_test( +envoy_cc_library( + name = "engine_builder_test_shim_lib", + hdrs = ["engine_builder_test_shim.h"], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + "//library/cc:mobile_engine_builder_lib", + ], +) + +envoy_cc_library( + name = "engine_builder_test_shim_legacy_lib", + hdrs = ["engine_builder_test_shim.h"], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + "//library/cc:engine_builder_lib", + ], +) + +envoy_cc_test_with_engine_builder( name = "engine_test", srcs = ["engine_test.cc"], env = {"ENVOY_NO_LOG_SINK": ""}, repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//test/common/integration:engine_with_test_server", "//test/common/integration:test_server_lib", diff --git a/mobile/test/cc/engine_builder_test_shim.h b/mobile/test/cc/engine_builder_test_shim.h new file mode 100644 index 0000000000000..82f6de0eb461a --- /dev/null +++ b/mobile/test/cc/engine_builder_test_shim.h @@ -0,0 +1,27 @@ +#pragma once + +#if defined(USE_MOBILE_ENGINE_BUILDER) + +#include "library/cc/mobile_engine_builder.h" + +namespace Envoy { +namespace Platform { +class EngineBuilder : public MobileEngineBuilder { +public: + using MobileEngineBuilder::MobileEngineBuilder; + + std::unique_ptr generateBootstrap() const { + auto* mutable_this = const_cast(this); + return mutable_this->Platform::EngineBuilderBase::generateBootstrap() + .value(); + } +}; +using EngineBuilderSharedPtr = std::shared_ptr; +} // namespace Platform +} // namespace Envoy + +#else + +#include "library/cc/engine_builder.h" + +#endif diff --git a/mobile/test/cc/engine_test.cc b/mobile/test/cc/engine_test.cc index 5beba1d965c04..071394d616745 100644 --- a/mobile/test/cc/engine_test.cc +++ b/mobile/test/cc/engine_test.cc @@ -5,7 +5,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" diff --git a/mobile/test/cc/integration/BUILD b/mobile/test/cc/integration/BUILD index 26924f1466bc2..c796518726d5e 100644 --- a/mobile/test/cc/integration/BUILD +++ b/mobile/test/cc/integration/BUILD @@ -1,15 +1,15 @@ -load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_mobile_package") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_cc_test_library", "envoy_mobile_package") +load("//bazel:envoy_mobile_cc_test.bzl", "envoy_cc_test_with_engine_builder") licenses(["notice"]) # Apache 2 envoy_mobile_package() -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "send_headers_test", srcs = ["send_headers_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/http/filters/assertion:filter_cc_proto", @@ -19,12 +19,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "send_trailers_test", srcs = ["send_trailers_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/http/filters/assertion:filter_cc_proto", @@ -34,12 +33,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "send_data_test", srcs = ["send_data_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/http/filters/assertion:filter_cc_proto", @@ -49,12 +47,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "receive_headers_test", srcs = ["receive_headers_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/integration:engine_with_test_server", @@ -63,12 +60,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "receive_data_test", srcs = ["receive_data_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/integration:engine_with_test_server", @@ -77,12 +73,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "receive_trailers_test", srcs = ["receive_trailers_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/integration:engine_with_test_server", @@ -91,12 +86,11 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "lifetimes_test", srcs = ["lifetimes_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_types_lib", "//library/common/http:header_utility_lib", "//test/common/integration:engine_with_test_server", @@ -104,3 +98,46 @@ envoy_cc_test( "@envoy_build_config//:test_extensions", ], ) + +envoy_cc_test_library( + name = "test_base_lib", + srcs = [ + "base/test_engine_and_server.cc", + ], + hdrs = [ + "base/test_engine_and_server.h", + "base/test_engine_builder.h", + ], + repository = "@envoy", + deps = [ + "//library/cc:engine_builder_base_lib", + "//library/cc:envoy_engine_cc_lib_no_stamp", + "//test/common/integration:test_server_lib", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/strings", + "@envoy//source/extensions/clusters/static:static_cluster_lib", + "@envoy//source/extensions/load_balancing_policies/round_robin:config", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/config/endpoint/v3:pkg_cc_proto", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/http_11_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/quic/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/raw_buffer/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/upstreams/http/v3:pkg_cc_proto", + ], +) + +envoy_cc_test( + name = "integration_test", + srcs = ["base/integration_test.cc"], + repository = "@envoy", + deps = [ + ":test_base_lib", + "//library/cc:envoy_engine_cc_lib_no_stamp", + "//test/common/http/filters/assertion:filter_cc_proto", + "@envoy//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + ], +) diff --git a/mobile/test/cc/integration/base/integration_test.cc b/mobile/test/cc/integration/base/integration_test.cc new file mode 100644 index 0000000000000..0e714cb161299 --- /dev/null +++ b/mobile/test/cc/integration/base/integration_test.cc @@ -0,0 +1,177 @@ +#include +#include +#include +#include + +#include "test/cc/integration/base/test_engine_builder.h" +#include "test/cc/integration/base/test_engine_and_server.h" +#include "test/test_common/utility.h" +#include "library/cc/stream_prototype.h" +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/buffer/buffer.h" +#include "envoy/http/header_map.h" +#include "library/cc/stream.h" +#include "library/common/engine_types.h" +#include "library/common/http/header_utility.h" +#include "library/common/types/c_types.h" +#include "test/common/http/filters/assertion/filter.pb.h" +#include "test/common/integration/test_server.h" +#include "source/common/buffer/buffer_impl.h" +#include "source/common/common/base_logger.h" + +namespace Envoy { +namespace { + +class BaseEngineBuilderTest : public testing::TestWithParam { +protected: + BaseEngineBuilderTest() : server_type_(GetParam()) {} + + void SetUpEngineAndServer( + std::optional assertion_config = + std::nullopt, + const absl::flat_hash_map& headers = {}, + absl::string_view body = "", + const absl::flat_hash_map& trailers = {}) { + absl::Notification engine_running; + Platform::TestEngineBuilder engine_builder; + + if (assertion_config.has_value()) { + ::envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter + assertion_filter; + assertion_filter.set_name("envoy.filters.http.assertion"); + std::ignore = assertion_filter.mutable_typed_config()->PackFrom(assertion_config.value()); + engine_builder.addHcmHttpFilter(std::move(assertion_filter)); + } + + engine_builder.enableLogger(false).setLogLevel(Logger::Logger::debug).setOnEngineRunning([&]() { + engine_running.Notify(); + }); + client_engine_with_test_server_ = std::make_unique( + engine_builder, server_type_, headers, body, trailers); + engine_running.WaitForNotification(); + } + + std::shared_ptr StartStream(Platform::StreamPrototypeSharedPtr stream_prototype, + bool end_stream = true) { + on_data_was_called_ = false; + on_headers_was_called_ = false; + on_trailers_was_called_ = false; + actual_status_code_ = ""; + actual_response_body_ = ""; + actual_trailer_value_ = ""; + actual_end_stream_ = false; + + EnvoyStreamCallbacks callbacks; + callbacks.on_headers_ = [this](const Http::ResponseHeaderMap& headers, bool end_stream, + envoy_stream_intel) { + on_headers_was_called_ = true; + actual_end_stream_ = end_stream; + auto status = headers.get(Http::LowerCaseString(":status")); + if (!status.empty()) { + actual_status_code_ = std::string(status[0]->value().getStringView()); + } + }; + callbacks.on_data_ = [this](const Buffer::Instance& data, uint64_t, bool end_stream, + envoy_stream_intel) { + on_data_was_called_ = true; + actual_end_stream_ = end_stream; + actual_response_body_ += data.toString(); + }; + callbacks.on_trailers_ = [this](const Http::ResponseTrailerMap& trailers, envoy_stream_intel) { + on_trailers_was_called_ = true; + actual_end_stream_ = true; + auto trailer = trailers.get(Http::LowerCaseString("test-trailer")); + if (!trailer.empty()) { + actual_trailer_value_ = std::string(trailer[0]->value().getStringView()); + } + }; + callbacks.on_complete_ = [this](envoy_stream_intel, envoy_final_stream_intel) { + stream_completed_.Notify(); + }; + + auto stream = stream_prototype->start(std::move(callbacks)); + + auto headers = std::make_unique(); + headers->addCopy(Http::LowerCaseString(":method"), "GET"); + headers->addCopy(Http::LowerCaseString(":scheme"), "http"); + headers->addCopy(Http::LowerCaseString(":authority"), + client_engine_with_test_server_->test_server().getAddress()); + headers->addCopy(Http::LowerCaseString(":path"), "/"); + stream->sendHeaders(std::move(headers), end_stream); + return stream; + } + + TestServerType server_type_; + std::unique_ptr client_engine_with_test_server_; + absl::Notification stream_completed_; + bool on_data_was_called_ = false; + bool on_headers_was_called_ = false; + bool on_trailers_was_called_ = false; + bool actual_end_stream_ = false; + std::string actual_status_code_; + std::string actual_trailer_value_; + std::string actual_response_body_; +}; + +INSTANTIATE_TEST_SUITE_P(BaseEngineBuilderTest, BaseEngineBuilderTest, + testing::Values(TestServerType::HTTP1_WITHOUT_TLS, + TestServerType::HTTP1_WITH_TLS, + TestServerType::HTTP2_WITH_TLS, TestServerType::HTTP3)); + +TEST_P(BaseEngineBuilderTest, GetRequest) { + SetUpEngineAndServer(/*assertion_config=*/std::nullopt, + /*headers=*/{{"test-header", "test-value"}}, + /*body=*/"test body"); + auto engine = client_engine_with_test_server_->engine(); + auto stream_prototype = engine->streamClient()->newStreamPrototype(); + StartStream(stream_prototype); + stream_completed_.WaitForNotification(); + + EXPECT_TRUE(on_headers_was_called_); + EXPECT_TRUE(on_data_was_called_); + EXPECT_TRUE(actual_end_stream_); + EXPECT_EQ(actual_status_code_, "200"); + EXPECT_EQ(actual_response_body_, "test body"); +} + +TEST_P(BaseEngineBuilderTest, GetRequestWithTrailers) { + SetUpEngineAndServer(/*assertion_config=*/std::nullopt, + /*headers=*/{{"test-header", "test-value"}}, + /*body=*/"test body", + /*trailers=*/{{"test-trailer", "test-trailer-value"}}); + auto engine = client_engine_with_test_server_->engine(); + auto stream_prototype = engine->streamClient()->newStreamPrototype(); + StartStream(stream_prototype); + stream_completed_.WaitForNotification(); + + EXPECT_TRUE(on_headers_was_called_); + EXPECT_TRUE(on_data_was_called_); + EXPECT_TRUE(on_trailers_was_called_); + EXPECT_TRUE(actual_end_stream_); + EXPECT_EQ(actual_status_code_, "200"); + EXPECT_EQ(actual_response_body_, "test body"); + EXPECT_EQ(actual_trailer_value_, "test-trailer-value"); +} + +TEST_P(BaseEngineBuilderTest, PostRequestWithBody) { + SetUpEngineAndServer(); + auto engine = client_engine_with_test_server_->engine(); + auto stream_prototype = engine->streamClient()->newStreamPrototype(); + auto stream = StartStream(stream_prototype, /*end_stream=*/false); + + auto buffer = std::make_unique("post body"); + stream->close(std::move(buffer)); + stream_completed_.WaitForNotification(); + + EXPECT_TRUE(on_headers_was_called_); + EXPECT_TRUE(on_data_was_called_); + EXPECT_TRUE(actual_end_stream_); + EXPECT_EQ(actual_status_code_, "200"); +} + +} // namespace +} // namespace Envoy diff --git a/mobile/test/cc/integration/base/test_engine_and_server.cc b/mobile/test/cc/integration/base/test_engine_and_server.cc new file mode 100644 index 0000000000000..592d4988fe830 --- /dev/null +++ b/mobile/test/cc/integration/base/test_engine_and_server.cc @@ -0,0 +1,123 @@ +#include "test/cc/integration/base/test_engine_and_server.h" + +#include +#include +#include + +#include "test/cc/integration/base/test_engine_builder.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "absl/strings/str_cat.h" +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/config/endpoint/v3/endpoint_components.pb.h" +#include "envoy/config/route/v3/route.pb.h" +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/extensions/transport_sockets/http_11_proxy/v3/upstream_http_11_connect.pb.h" +#include "envoy/extensions/transport_sockets/quic/v3/quic_transport.pb.h" +#include "envoy/extensions/transport_sockets/tls/v3/tls.pb.h" +#include "envoy/extensions/transport_sockets/raw_buffer/v3/raw_buffer.pb.h" +#include "envoy/extensions/upstreams/http/v3/http_protocol_options.pb.h" +#include "test/common/integration/test_server.h" +#include "source/common/common/assert.h" + +namespace Envoy { + +TestEngineAndServer::TestEngineAndServer( + Platform::TestEngineBuilder& engine_builder, TestServerType type, + const absl::flat_hash_map& headers, absl::string_view body, + const absl::flat_hash_map& trailers) { + test_server_.start(type); + test_server_.setResponse(headers, body, trailers); + + // Set up route configuration and cluster for ClientEngineBuilder to route + // requests to test_server_. + ::envoy::config::route::v3::RouteConfiguration route_configuration; + route_configuration.set_name("route_config"); + auto* virtual_host = route_configuration.add_virtual_hosts(); + virtual_host->set_name("test_virtual_host"); + virtual_host->add_domains("*"); + auto* route = virtual_host->add_routes(); + route->mutable_match()->set_prefix("/"); + route->mutable_route()->set_cluster("test_cluster"); + engine_builder.setHcmRouteConfiguration(std::move(route_configuration)); + + ::envoy::config::cluster::v3::Cluster cluster; + cluster.set_name("test_cluster"); + cluster.mutable_connect_timeout()->set_seconds(5); + auto* endpoint = + cluster.mutable_load_assignment()->add_endpoints()->add_lb_endpoints()->mutable_endpoint(); + endpoint->mutable_address()->mutable_socket_address()->set_address(test_server_.getIpAddress()); + endpoint->mutable_address()->mutable_socket_address()->set_port_value(test_server_.getPort()); + cluster.mutable_load_assignment()->set_cluster_name("test_cluster"); + cluster.set_type(::envoy::config::cluster::v3::Cluster::STATIC); + + bool use_tls = type == TestServerType::HTTP1_WITH_TLS || type == TestServerType::HTTP2_WITH_TLS || + type == TestServerType::HTTP3; + + ::envoy::extensions::upstreams::http::v3::HttpProtocolOptions protocol_options; + + if (use_tls) { + ::envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext tls_context; + tls_context.mutable_common_tls_context() + ->mutable_validation_context() + ->set_trust_chain_verification(::envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::ACCEPT_UNTRUSTED); + tls_context.set_sni("www.lyft.com"); + + if (type == TestServerType::HTTP2_WITH_TLS) { + tls_context.mutable_common_tls_context()->add_alpn_protocols("h2"); + protocol_options.mutable_explicit_http_config()->mutable_http2_protocol_options(); + } else if (type == TestServerType::HTTP1_WITH_TLS) { + tls_context.mutable_common_tls_context()->add_alpn_protocols("http/1.1"); + protocol_options.mutable_explicit_http_config() + ->mutable_http_protocol_options() + ->set_enable_trailers(true); + } + cluster.mutable_transport_socket()->set_name("envoy.transport_sockets.tls"); + std::ignore = cluster.mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_context); + + if (type == TestServerType::HTTP3) { + protocol_options.mutable_explicit_http_config()->mutable_http3_protocol_options(); + ::envoy::extensions::transport_sockets::quic::v3::QuicUpstreamTransport h3_inner_socket; + h3_inner_socket.mutable_upstream_tls_context()->CopyFrom(tls_context); + ::envoy::extensions::transport_sockets::http_11_proxy::v3::Http11ProxyUpstreamTransport + h3_proxy_socket; + std::ignore = h3_proxy_socket.mutable_transport_socket()->mutable_typed_config()->PackFrom( + h3_inner_socket); + h3_proxy_socket.mutable_transport_socket()->set_name("envoy.transport_sockets.quic"); + cluster.mutable_transport_socket()->set_name("envoy.transport_sockets.http_11_proxy"); + std::ignore = + cluster.mutable_transport_socket()->mutable_typed_config()->PackFrom(h3_proxy_socket); + } + } else { + cluster.mutable_transport_socket()->set_name("envoy.transport_sockets.raw_buffer"); + ::envoy::extensions::transport_sockets::raw_buffer::v3::RawBuffer raw_buffer; + std::ignore = cluster.mutable_transport_socket()->mutable_typed_config()->PackFrom(raw_buffer); + protocol_options.mutable_explicit_http_config() + ->mutable_http_protocol_options() + ->set_enable_trailers(true); + } + + std::ignore = (*cluster.mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(protocol_options); + + engine_builder.addCluster(std::move(cluster)); + + auto engine = engine_builder.build(); + RELEASE_ASSERT(engine.ok(), absl::StrCat("Failed to build engine: ", engine.status().message())); + engine_ = *std::move(engine); +} + +TestEngineAndServer::~TestEngineAndServer() { + test_server_.shutdown(); + if (engine_ != nullptr) { + engine_->terminate(); + } +} + +std::shared_ptr TestEngineAndServer::engine() { return engine_; } + +TestServer& TestEngineAndServer::test_server() { return test_server_; } + +} // namespace Envoy diff --git a/mobile/test/cc/integration/base/test_engine_and_server.h b/mobile/test/cc/integration/base/test_engine_and_server.h new file mode 100644 index 0000000000000..8042d5af84299 --- /dev/null +++ b/mobile/test/cc/integration/base/test_engine_and_server.h @@ -0,0 +1,37 @@ +#pragma once + +#ifndef TEST_CC_INTEGRATION_BASE_TEST_ENGINE_AND_SERVER_H_ +#define TEST_CC_INTEGRATION_BASE_TEST_ENGINE_AND_SERVER_H_ + +#include +#include + +#include "test/cc/integration/base/test_engine_builder.h" +#include "library/cc/engine.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "test/common/integration/test_server.h" + +namespace Envoy { + +// Test only class. +class TestEngineAndServer { +public: + TestEngineAndServer(Platform::TestEngineBuilder& engine_builder, TestServerType type, + const absl::flat_hash_map& headers = {}, + absl::string_view body = "", + const absl::flat_hash_map& trailers = {}); + ~TestEngineAndServer(); + + std::shared_ptr engine(); + + TestServer& test_server(); + +private: + std::shared_ptr engine_; + TestServer test_server_; +}; + +} // namespace Envoy + +#endif // TEST_CC_INTEGRATION_BASE_TEST_ENGINE_AND_SERVER_H_ diff --git a/mobile/test/cc/integration/base/test_engine_builder.h b/mobile/test/cc/integration/base/test_engine_builder.h new file mode 100644 index 0000000000000..7f112652ce219 --- /dev/null +++ b/mobile/test/cc/integration/base/test_engine_builder.h @@ -0,0 +1,86 @@ +#pragma once + +#include "library/cc/engine_builder_base.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" + +#include "source/extensions/clusters/static/static_cluster.h" +#include "source/extensions/load_balancing_policies/round_robin/config.h" + +namespace Envoy { +namespace Platform { + +class TestEngineBuilder : public EngineBuilderBase { + friend class EngineBuilderBase; + +public: + TestEngineBuilder() = default; + + TestEngineBuilder& addHcmHttpFilter( + ::envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter filter) { + hcm_http_filters_.push_back(std::move(filter)); + return *this; + } + + TestEngineBuilder& + setHcmRouteConfiguration(::envoy::config::route::v3::RouteConfiguration route_configuration) { + route_configuration_ = std::move(route_configuration); + return *this; + } + + TestEngineBuilder& addCluster(::envoy::config::cluster::v3::Cluster cluster) { + clusters_.push_back(std::move(cluster)); + return *this; + } + +private: + // Hooks required by EngineBuilderBase + void preRunSetup(InternalEngine* engine) { + (void)engine; + Envoy::Upstream::forceRegisterStaticClusterFactory(); + Envoy::Extensions::LoadBalancingPolicies::RoundRobin::forceRegisterFactory(); + } + void postRunSetup(Engine*) {} + + absl::Status configXds(envoy::config::bootstrap::v3::Bootstrap*) { return absl::OkStatus(); } + absl::Status configureNode(envoy::config::core::v3::Node* node) { + node->set_id("envoy-test-client"); + node->set_cluster("envoy-test-client"); + return absl::OkStatus(); + } + + absl::Status configureRouteConfig(envoy::config::route::v3::RouteConfiguration* route_config) { + if (route_configuration_.has_value()) { + route_config->Swap(&route_configuration_.value()); + } + return absl::OkStatus(); + } + + absl::Status configureStaticClusters( + Protobuf::RepeatedPtrField* clusters) { + for (auto& cluster : clusters_) { + *clusters->Add() = std::move(cluster); + } + clusters_.clear(); + return absl::OkStatus(); + } + + absl::Status configureHttpFilters( + std::function + add_filter) { + for (auto& filter : hcm_http_filters_) { + *add_filter() = std::move(filter); + } + hcm_http_filters_.clear(); + return absl::OkStatus(); + } + + void configureCustomRouterFilter(::envoy::extensions::filters::http::router::v3::Router&) {} + + std::vector<::envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter> + hcm_http_filters_; + std::optional<::envoy::config::route::v3::RouteConfiguration> route_configuration_; + std::vector<::envoy::config::cluster::v3::Cluster> clusters_; +}; + +} // namespace Platform +} // namespace Envoy diff --git a/mobile/test/cc/integration/lifetimes_test.cc b/mobile/test/cc/integration/lifetimes_test.cc index 1c15661060779..3fd5f27e5726e 100644 --- a/mobile/test/cc/integration/lifetimes_test.cc +++ b/mobile/test/cc/integration/lifetimes_test.cc @@ -3,7 +3,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" diff --git a/mobile/test/cc/integration/receive_data_test.cc b/mobile/test/cc/integration/receive_data_test.cc index 8301b8b6baad4..aac34c744a81e 100644 --- a/mobile/test/cc/integration/receive_data_test.cc +++ b/mobile/test/cc/integration/receive_data_test.cc @@ -3,7 +3,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" diff --git a/mobile/test/cc/integration/receive_headers_test.cc b/mobile/test/cc/integration/receive_headers_test.cc index c124f31ae1ba1..7e8f341c60b87 100644 --- a/mobile/test/cc/integration/receive_headers_test.cc +++ b/mobile/test/cc/integration/receive_headers_test.cc @@ -3,7 +3,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" diff --git a/mobile/test/cc/integration/receive_trailers_test.cc b/mobile/test/cc/integration/receive_trailers_test.cc index c7383c47744f4..cb67bb10be232 100644 --- a/mobile/test/cc/integration/receive_trailers_test.cc +++ b/mobile/test/cc/integration/receive_trailers_test.cc @@ -3,7 +3,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" diff --git a/mobile/test/cc/integration/send_data_test.cc b/mobile/test/cc/integration/send_data_test.cc index b20723794bd46..32ec034d6b1c7 100644 --- a/mobile/test/cc/integration/send_data_test.cc +++ b/mobile/test/cc/integration/send_data_test.cc @@ -4,7 +4,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" @@ -19,7 +19,7 @@ TEST(SendDataTest, Success) { typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; - assertion.SerializeToString(&serialized_assertion); + std::ignore = assertion.SerializeToString(&serialized_assertion); typed_config.set_value(serialized_assertion); absl::Notification engine_running; diff --git a/mobile/test/cc/integration/send_headers_test.cc b/mobile/test/cc/integration/send_headers_test.cc index 25bdf2c623456..578f8506a4945 100644 --- a/mobile/test/cc/integration/send_headers_test.cc +++ b/mobile/test/cc/integration/send_headers_test.cc @@ -4,7 +4,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" @@ -27,7 +27,7 @@ TEST(SendHeadersTest, Success) { typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; - assertion.SerializeToString(&serialized_assertion); + std::ignore = assertion.SerializeToString(&serialized_assertion); typed_config.set_value(serialized_assertion); absl::Notification engine_running; diff --git a/mobile/test/cc/integration/send_trailers_test.cc b/mobile/test/cc/integration/send_trailers_test.cc index bcf75c9134ad6..0361847357e53 100644 --- a/mobile/test/cc/integration/send_trailers_test.cc +++ b/mobile/test/cc/integration/send_trailers_test.cc @@ -4,7 +4,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_types.h" #include "library/common/http/header_utility.h" @@ -21,7 +21,7 @@ TEST(SendTrailersTest, Success) { typed_config.set_type_url( "type.googleapis.com/envoymobile.extensions.filters.http.assertion.Assertion"); std::string serialized_assertion; - assertion.SerializeToString(&serialized_assertion); + std::ignore = assertion.SerializeToString(&serialized_assertion); typed_config.set_value(serialized_assertion); absl::Notification engine_running; diff --git a/mobile/test/cc/unit/BUILD b/mobile/test/cc/unit/BUILD index 988e3849e47b8..bf3ed7022cb27 100644 --- a/mobile/test/cc/unit/BUILD +++ b/mobile/test/cc/unit/BUILD @@ -1,10 +1,11 @@ -load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_mobile_package") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") +load("//bazel:envoy_mobile_cc_test.bzl", "envoy_cc_test_with_engine_builder") licenses(["notice"]) # Apache 2 envoy_mobile_package() -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "envoy_config_test", srcs = ["envoy_config_test.cc"], linkopts = select({ @@ -17,7 +18,6 @@ envoy_cc_test( }), repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/cc:envoy_engine_cc_lib_no_stamp", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", @@ -28,7 +28,7 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "fetch_client_test", srcs = ["fetch_client_test.cc"], exec_properties = { @@ -36,18 +36,16 @@ envoy_cc_test( }, repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "@envoy_build_config//:extension_registry", "@envoy_build_config//:test_extensions", ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "engine_handle_test", srcs = ["engine_handle_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:internal_engine_lib_no_stamp", "@envoy_build_config//:extension_registry", "@envoy_build_config//:test_extensions", diff --git a/mobile/test/cc/unit/engine_handle_test.cc b/mobile/test/cc/unit/engine_handle_test.cc index e18040674bbae..ea972767f6dc9 100644 --- a/mobile/test/cc/unit/engine_handle_test.cc +++ b/mobile/test/cc/unit/engine_handle_test.cc @@ -1,6 +1,6 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/internal_engine.h" namespace Envoy { diff --git a/mobile/test/cc/unit/envoy_config_test.cc b/mobile/test/cc/unit/envoy_config_test.cc index a9c3e4560c38b..fe3e19c774bdc 100644 --- a/mobile/test/cc/unit/envoy_config_test.cc +++ b/mobile/test/cc/unit/envoy_config_test.cc @@ -1,20 +1,22 @@ #include +#include #include +#include #include +#include "gmock/gmock.h" #include "envoy/config/cluster/v3/cluster.pb.h" #include "envoy/config/core/v3/socket_option.pb.h" #include "envoy/extensions/clusters/dynamic_forward_proxy/v3/cluster.pb.h" #include "envoy/extensions/filters/http/buffer/v3/buffer.pb.h" +#include "source/common/protobuf/utility.h" #include "test/test_common/utility.h" #include "absl/strings/str_cat.h" -#include "absl/strings/str_replace.h" -#include "absl/synchronization/notification.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/api/external.h" #include "library/common/bridge//utility.h" @@ -32,10 +34,8 @@ using envoy::config::cluster::v3::Cluster; using envoy::config::core::v3::SocketOption; using DfpClusterConfig = ::envoy::extensions::clusters::dynamic_forward_proxy::v3::ClusterConfig; using testing::HasSubstr; -using testing::IsEmpty; using testing::Not; using testing::NotNull; -using testing::SizeIs; DfpClusterConfig getDfpClusterConfig(const Bootstrap& bootstrap) { DfpClusterConfig cluster_config; @@ -86,7 +86,8 @@ TEST(TestConfig, ConfigIsApplied) { .addRuntimeGuard("quic_no_tcp_delay", true) .enableDnsCache(true, /* save_interval_seconds */ 101) .addDnsPreresolveHostnames({"lyft.com", "google.com"}) - .setDeviceOs("probably-ubuntu-on-CI"); + .setDeviceOs("probably-ubuntu-on-CI") + .enableScone(true); std::unique_ptr bootstrap = engine_builder.generateBootstrap(); const std::string config_str = bootstrap->ShortDebugString(); @@ -113,6 +114,7 @@ TEST(TestConfig, ConfigIsApplied) { "key: \"app_version\" value { string_value: \"1.2.3\" } }", "key: \"app_id\" value { string_value: \"1234-1234-1234\" } }", "initial_stream_window_size { value: 6291456 }", + "enable_scone { value: true }", "initial_connection_window_size { value: 15728640 }"}; for (const auto& string : must_contain) { @@ -213,6 +215,17 @@ TEST(TestConfig, StreamIdleTimeout) { EXPECT_THAT(bootstrap->ShortDebugString(), HasSubstr("stream_idle_timeout { seconds: 42 }")); } +TEST(TestConfig, RequestTimeout) { + EngineBuilder engine_builder; + + std::unique_ptr bootstrap = engine_builder.generateBootstrap(); + EXPECT_THAT(bootstrap->ShortDebugString(), HasSubstr("timeout { }")); + + engine_builder.setRequestTimeoutMilliseconds(42500); + bootstrap = engine_builder.generateBootstrap(); + EXPECT_THAT(bootstrap->ShortDebugString(), HasSubstr("timeout { seconds: 42 nanos: 500000000 }")); +} + TEST(TestConfig, PerTryIdleTimeout) { EngineBuilder engine_builder; @@ -335,6 +348,17 @@ TEST(TestConfig, DisableHttp3) { Not(HasSubstr("envoy.extensions.filters.http.alternate_protocols_cache.v3.FilterConfig"))); } +TEST(TestConfig, EnableEarlyData) { + EngineBuilder engine_builder; + + std::unique_ptr bootstrap = engine_builder.generateBootstrap(); + EXPECT_THAT(bootstrap->ShortDebugString(), Not(HasSubstr("early_data_policy"))); + + engine_builder.enableEarlyData(false); + bootstrap = engine_builder.generateBootstrap(); + EXPECT_THAT(bootstrap->ShortDebugString(), HasSubstr("envoy.route.early_data_policy.default")); +} + TEST(TestConfig, UdpSocketReceiveBufferSize) { EngineBuilder engine_builder; engine_builder.enableHttp3(true); @@ -477,7 +501,7 @@ TEST(TestConfig, AddNativeFilters) { Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"); std::string serialized_buffer; - buffer.SerializeToString(&serialized_buffer); + std::ignore = buffer.SerializeToString(&serialized_buffer); typed_config.set_value(serialized_buffer); engine_builder.addNativeFilter(filter_name1, typed_config); @@ -684,5 +708,90 @@ TEST(TestConfig, MoveConstructor) { } #endif // ENVOY_MOBILE_XDS +#if defined(USE_MOBILE_ENGINE_BUILDER) +TEST(TestConfig, CustomListenerSuccess) { + MobileEngineBuilder builder; + envoy::config::listener::v3::Listener listener; + listener.set_name("custom_listener_1"); + listener.mutable_api_listener(); // set api_listener + builder.addApiListener(listener); + + auto bootstrap_or_status = builder.generateBootstrap(); + ASSERT_TRUE(bootstrap_or_status.ok()); + auto bootstrap = std::move(bootstrap_or_status.value()); + + ASSERT_EQ(bootstrap->static_resources().listeners_size(), 1); + EXPECT_EQ(bootstrap->static_resources().listeners(0).name(), "custom_listener_1"); + EXPECT_TRUE(bootstrap->static_resources().listeners(0).has_api_listener()); +} + +TEST(TestConfig, CustomListenerValidationEmptyName) { + MobileEngineBuilder builder; + envoy::config::listener::v3::Listener listener; + // name is empty + listener.mutable_api_listener(); + builder.addApiListener(listener); + + auto bootstrap_or_status = builder.generateBootstrap(); + EXPECT_FALSE(bootstrap_or_status.ok()); + EXPECT_EQ(bootstrap_or_status.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(bootstrap_or_status.status().message(), + HasSubstr("Custom listener must have a name.")); +} + +TEST(TestConfig, CustomListenerValidationMissingApiListener) { + MobileEngineBuilder builder; + envoy::config::listener::v3::Listener listener; + listener.set_name("custom_listener_1"); + // api_listener is not set + builder.addApiListener(listener); + + auto bootstrap_or_status = builder.generateBootstrap(); + EXPECT_FALSE(bootstrap_or_status.ok()); + EXPECT_EQ(bootstrap_or_status.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(bootstrap_or_status.status().message(), HasSubstr("must have an api_listener")); +} + +TEST(TestConfig, CustomListenerValidationDuplicateName) { + MobileEngineBuilder builder; + envoy::config::listener::v3::Listener listener1; + listener1.set_name("custom_listener_1"); + listener1.mutable_api_listener(); + builder.addApiListener(listener1); + + envoy::config::listener::v3::Listener listener2; + listener2.set_name("custom_listener_1"); // duplicate name + listener2.mutable_api_listener(); + builder.addApiListener(listener2); + + auto bootstrap_or_status = builder.generateBootstrap(); + EXPECT_FALSE(bootstrap_or_status.ok()); + EXPECT_EQ(bootstrap_or_status.status().code(), absl::StatusCode::kInvalidArgument); + EXPECT_THAT(bootstrap_or_status.status().message(), + HasSubstr("Duplicate listener name: custom_listener_1")); +} + +TEST(TestConfig, MultipleCustomListeners) { + MobileEngineBuilder builder; + envoy::config::listener::v3::Listener listener1; + listener1.set_name("custom_listener_1"); + listener1.mutable_api_listener(); + builder.addApiListener(listener1); + + envoy::config::listener::v3::Listener listener2; + listener2.set_name("custom_listener_2"); + listener2.mutable_api_listener(); + builder.addApiListener(listener2); + + auto bootstrap_or_status = builder.generateBootstrap(); + ASSERT_TRUE(bootstrap_or_status.ok()); + auto bootstrap = std::move(bootstrap_or_status.value()); + + ASSERT_EQ(bootstrap->static_resources().listeners_size(), 2); + EXPECT_EQ(bootstrap->static_resources().listeners(0).name(), "custom_listener_1"); + EXPECT_EQ(bootstrap->static_resources().listeners(1).name(), "custom_listener_2"); +} +#endif + } // namespace } // namespace Envoy diff --git a/mobile/test/cc/unit/fetch_client_test.cc b/mobile/test/cc/unit/fetch_client_test.cc index 6adc63ca5415d..8d97720f4af8e 100644 --- a/mobile/test/cc/unit/fetch_client_test.cc +++ b/mobile/test/cc/unit/fetch_client_test.cc @@ -8,7 +8,7 @@ #include "absl/synchronization/notification.h" #include "gtest/gtest.h" #include "library/cc/engine.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/cc/stream.h" #include "library/cc/stream_client.h" #include "library/cc/stream_prototype.h" @@ -23,7 +23,7 @@ envoy_status_t fetchUrls(const std::vector urls, std::vector* used_protocols) { absl::Notification engine_running; Platform::EngineBuilder engine_builder; - engine_builder.setLogLevel(Envoy::Logger::Logger::trace) + engine_builder.setLogLevel(Envoy::Logger::Logger::info) .enableLogger(false) .addRuntimeGuard("dns_cache_set_ip_version_to_remove", true) .addRuntimeGuard("quic_no_tcp_delay", true) diff --git a/mobile/test/common/BUILD b/mobile/test/common/BUILD index 5e42141543206..e977977e12ff9 100644 --- a/mobile/test/common/BUILD +++ b/mobile/test/common/BUILD @@ -1,26 +1,25 @@ -load("@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_mobile_package") +load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") +load("//bazel:envoy_mobile_cc_test.bzl", "envoy_cc_test_with_engine_builder") licenses(["notice"]) # Apache 2 envoy_mobile_package() -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "engine_common_test", srcs = ["engine_common_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:engine_common_lib", "@envoy_build_config//:extension_registry", ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "internal_engine_test", srcs = ["internal_engine_test.cc"], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common:internal_engine_lib_no_stamp", "//library/common/bridge:utility_lib", "//library/common/http:header_utility_lib", diff --git a/mobile/test/common/engine_common_test.cc b/mobile/test/common/engine_common_test.cc index 6e2f1737c66b9..b4dff6b6897f1 100644 --- a/mobile/test/common/engine_common_test.cc +++ b/mobile/test/common/engine_common_test.cc @@ -1,6 +1,6 @@ #include "extension_registry.h" #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/engine_common.h" namespace Envoy { diff --git a/mobile/test/common/extensions/cert_validator/platform_bridge/BUILD b/mobile/test/common/extensions/cert_validator/platform_bridge/BUILD index 03c5cb928d0bf..1abd48daea4de 100644 --- a/mobile/test/common/extensions/cert_validator/platform_bridge/BUILD +++ b/mobile/test/common/extensions/cert_validator/platform_bridge/BUILD @@ -20,11 +20,11 @@ envoy_extension_cc_test( "//library/common/extensions/cert_validator/platform_bridge:config", "//library/common/extensions/cert_validator/platform_bridge:platform_bridge_cc_proto", "//test/common/mocks/common:common_mocks", + "@envoy//source/common/crypto:utility_lib", "@envoy//source/common/tls/cert_validator:cert_validator_lib", "@envoy//test/common/tls:ssl_test_utils", "@envoy//test/common/tls/cert_validator:test_common", "@envoy//test/common/tls/test_data:cert_infos", - "@envoy//test/mocks/event:event_mocks", "@envoy//test/mocks/ssl:ssl_mocks", "@envoy//test/mocks/thread:thread_mocks", "@envoy//test/test_common:environment_lib", diff --git a/mobile/test/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator_test.cc b/mobile/test/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator_test.cc index 16b3cfa6d8008..eae21e4a4424f 100644 --- a/mobile/test/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator_test.cc +++ b/mobile/test/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator_test.cc @@ -13,7 +13,6 @@ #include "test/common/tls/cert_validator/test_common.h" #include "test/common/tls/ssl_test_utility.h" #include "test/common/tls/test_data/san_dns2_cert_info.h" -#include "test/mocks/event/mocks.h" #include "test/mocks/ssl/mocks.h" #include "test/mocks/thread/mocks.h" #include "test/test_common/environment.h" @@ -162,7 +161,7 @@ class PlatformBridgeCertValidatorTest std::unique_ptr mock_validator_; Thread::ThreadId main_thread_id_; std::unique_ptr helper_handle_; - absl::optional platform_bridge_config_; + std::optional platform_bridge_config_; }; INSTANTIATE_TEST_SUITE_P(TrustMode, PlatformBridgeCertValidatorTest, @@ -460,7 +459,7 @@ TEST_P(PlatformBridgeCertValidatorTest, ThreadPriority) { platform_bridge_config.mutable_thread_priority()->set_value(expected_thread_priority); envoy::config::core::v3::TypedExtensionConfig typed_config; typed_config.set_name("PlatformBridgeCertValidator"); - typed_config.mutable_typed_config()->PackFrom(platform_bridge_config); + std::ignore = typed_config.mutable_typed_config()->PackFrom(platform_bridge_config); platform_bridge_config_ = std::move(typed_config); EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()); diff --git a/mobile/test/common/extensions/filters/http/network_configuration/network_configuration_filter_test.cc b/mobile/test/common/extensions/filters/http/network_configuration/network_configuration_filter_test.cc index c76f5a176c528..a863b23e62921 100644 --- a/mobile/test/common/extensions/filters/http/network_configuration/network_configuration_filter_test.cc +++ b/mobile/test/common/extensions/filters/http/network_configuration/network_configuration_filter_test.cc @@ -3,7 +3,6 @@ #include "test/extensions/common/dynamic_forward_proxy/mocks.h" #include "test/mocks/event/mocks.h" #include "test/mocks/http/mocks.h" -#include "test/mocks/server/factory_context.h" #include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -155,7 +154,7 @@ TEST_F(NetworkConfigurationFilterTest, HostnameDnsLookupFail) { EXPECT_CALL(decoder_callbacks_.stream_info_, filterState()).Times(0); EXPECT_CALL(*dns_cache_, loadDnsCacheEntry_(Eq("localhost"), 82, false, _)) .WillOnce(Return(MockDnsCache::MockLoadDnsCacheEntryResult{ - DnsCache::LoadDnsCacheEntryStatus::Overflow, nullptr, absl::nullopt})); + DnsCache::LoadDnsCacheEntryStatus::Overflow, nullptr, std::nullopt})); EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_.decodeHeaders(default_request_headers_, false)); } @@ -174,7 +173,7 @@ TEST_F(NetworkConfigurationFilterTest, AsyncDnsLookupSuccess) { .WillOnce( Invoke([&](absl::string_view, uint16_t, bool, DnsCache::LoadDnsCacheEntryCallbacks&) { return MockDnsCache::MockLoadDnsCacheEntryResult{ - DnsCache::LoadDnsCacheEntryStatus::Loading, handle, absl::nullopt}; + DnsCache::LoadDnsCacheEntryStatus::Loading, handle, std::nullopt}; })); EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark, filter_.decodeHeaders(default_request_headers_, false)); diff --git a/mobile/test/common/extensions/key_value/platform/platform_store_test.cc b/mobile/test/common/extensions/key_value/platform/platform_store_test.cc index 242cdd0578af8..80daeb8adbaea 100644 --- a/mobile/test/common/extensions/key_value/platform/platform_store_test.cc +++ b/mobile/test/common/extensions/key_value/platform/platform_store_test.cc @@ -55,23 +55,23 @@ class PlatformStoreTest : public testing::Test { }; TEST_F(PlatformStoreTest, Basic) { - EXPECT_EQ(absl::nullopt, store_->get("foo")); - store_->addOrUpdate("foo", "bar", absl::nullopt); + EXPECT_EQ(std::nullopt, store_->get("foo")); + store_->addOrUpdate("foo", "bar", std::nullopt); EXPECT_EQ("bar", store_->get("foo").value()); - store_->addOrUpdate("foo", "eep", absl::nullopt); + store_->addOrUpdate("foo", "eep", std::nullopt); EXPECT_EQ("eep", store_->get("foo").value()); store_->remove("foo"); - EXPECT_EQ(absl::nullopt, store_->get("foo")); + EXPECT_EQ(std::nullopt, store_->get("foo")); } TEST_F(PlatformStoreTest, Persist) { - store_->addOrUpdate("foo", "bar", absl::nullopt); - store_->addOrUpdate("by\nz", "ee\np", absl::nullopt); + store_->addOrUpdate("foo", "bar", std::nullopt); + store_->addOrUpdate("by\nz", "ee\np", std::nullopt); ASSERT_TRUE(flush_timer_->enabled_); flush_timer_->invokeCallback(); // flush EXPECT_TRUE(flush_timer_->enabled_); // Not flushed as 5ms didn't pass. - store_->addOrUpdate("baz", "eep", absl::nullopt); + store_->addOrUpdate("baz", "eep", std::nullopt); save_interval_ = std::chrono::seconds(0); createStore(); @@ -86,7 +86,7 @@ TEST_F(PlatformStoreTest, Persist) { store_->iterate(validate); // This will flush due to 0ms flush interval - store_->addOrUpdate("baz", "eep", absl::nullopt); + store_->addOrUpdate("baz", "eep", std::nullopt); createStore(); EXPECT_TRUE(store_->get("baz").has_value()); @@ -97,8 +97,8 @@ TEST_F(PlatformStoreTest, Persist) { } TEST_F(PlatformStoreTest, Iterate) { - store_->addOrUpdate("foo", "bar", absl::nullopt); - store_->addOrUpdate("baz", "eep", absl::nullopt); + store_->addOrUpdate("foo", "bar", std::nullopt); + store_->addOrUpdate("baz", "eep", std::nullopt); int full_counter = 0; KeyValueStore::ConstIterateCb validate = [&full_counter](const std::string& key, diff --git a/mobile/test/common/extensions/quic_packet_writer/platform/platform_packet_writer_factory_test.cc b/mobile/test/common/extensions/quic_packet_writer/platform/platform_packet_writer_factory_test.cc index 0f747aef5321f..f7a9588d7c86f 100644 --- a/mobile/test/common/extensions/quic_packet_writer/platform/platform_packet_writer_factory_test.cc +++ b/mobile/test/common/extensions/quic_packet_writer/platform/platform_packet_writer_factory_test.cc @@ -59,7 +59,7 @@ class QuicPlatformPacketWriterFactoryTest : public ::testing::Test { void TearDown() override { injector_.reset(); } void writePacketAndVerifyResult(quic::WriteStatus expected_status, - absl::optional expected_error_code = std::nullopt) { + std::optional expected_error_code = std::nullopt) { auto result = packet_writer_->WritePacket(packet_data_.data(), packet_data_.length(), self_ip_, peer_addr_, nullptr, {}); EXPECT_EQ(expected_status, result.status); diff --git a/mobile/test/common/extensions/retry/options/network_configuration/predicate_test.cc b/mobile/test/common/extensions/retry/options/network_configuration/predicate_test.cc index 4836c7396ccc2..f8d15ac37b3c2 100644 --- a/mobile/test/common/extensions/retry/options/network_configuration/predicate_test.cc +++ b/mobile/test/common/extensions/retry/options/network_configuration/predicate_test.cc @@ -41,7 +41,7 @@ TEST(NetworkConfigurationRetryOptionsPredicateTest, PredicateTest) { auto proto_config = factory->createEmptyConfigProto(); auto predicate = factory->createOptionsPredicate(*proto_config, retry_extension_factory_context); - ASSERT_EQ(absl::nullopt, + ASSERT_EQ(std::nullopt, predicate->updateOptions({mock_stream_info, nullptr}).new_upstream_socket_options_); } diff --git a/mobile/test/common/extensions/stat_sinks/metrics_service/BUILD b/mobile/test/common/extensions/stat_sinks/metrics_service/BUILD index 0e23af06dbb1e..514b856640547 100644 --- a/mobile/test/common/extensions/stat_sinks/metrics_service/BUILD +++ b/mobile/test/common/extensions/stat_sinks/metrics_service/BUILD @@ -18,7 +18,6 @@ envoy_extension_cc_test( "//library/common/extensions/stat_sinks/metrics_service:service_cc_proto", "@envoy//test/mocks/grpc:grpc_mocks", "@envoy//test/mocks/local_info:local_info_mocks", - "@envoy//test/mocks/thread_local:thread_local_mocks", "@envoy//test/test_common:logging_lib", ], ) @@ -35,7 +34,6 @@ envoy_extension_cc_test( "@envoy//test/integration:http_integration_lib_light", "@envoy//test/mocks/grpc:grpc_mocks", "@envoy//test/mocks/local_info:local_info_mocks", - "@envoy//test/mocks/thread_local:thread_local_mocks", "@envoy//test/test_common:utility_lib", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", ], diff --git a/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_integration_test.cc b/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_integration_test.cc index 36afe1f174f92..64cb44100a981 100644 --- a/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_integration_test.cc +++ b/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_integration_test.cc @@ -11,6 +11,8 @@ #include "library/common/extensions/stat_sinks/metrics_service/service.pb.h" using testing::AssertionResult; +using testing::Eq; +using testing::Ge; namespace Envoy { namespace { @@ -40,7 +42,7 @@ class EnvoyMobileMetricsServiceIntegrationTest envoymobile::extensions::stat_sinks::metrics_service::EnvoyMobileMetricsServiceConfig config; setGrpcService(*config.mutable_grpc_service(), "metrics_service", fake_upstreams_.back()->localAddress()); - metrics_sink->mutable_typed_config()->PackFrom(config); + std::ignore = metrics_sink->mutable_typed_config()->PackFrom(config); // Shrink reporting period down to 1s to make test not take forever. bootstrap.mutable_stats_flush_interval()->CopyFrom( Protobuf::util::TimeUtil::MillisecondsToDuration(100)); @@ -180,10 +182,10 @@ TEST_P(EnvoyMobileMetricsServiceIntegrationTest, BasicFlow) { switch (clientType()) { case Grpc::ClientType::EnvoyGrpc: - test_server_->waitForGaugeEq("cluster.metrics_service.upstream_rq_active", 0); + test_server_->waitForGauge("cluster.metrics_service.upstream_rq_active", Eq(0)); break; case Grpc::ClientType::GoogleGrpc: - test_server_->waitForCounterGe("grpc.metrics_service.streams_closed_0", 1); + test_server_->waitForCounter("grpc.metrics_service.streams_closed_0", Ge(1)); break; default: PANIC("not implemented"); diff --git a/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_test.cc b/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_test.cc index e287cdfb956c8..3c50d083f3851 100644 --- a/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_test.cc +++ b/mobile/test/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer_test.cc @@ -4,9 +4,7 @@ #include "test/mocks/grpc/mocks.h" #include "test/mocks/local_info/mocks.h" #include "test/mocks/stats/mocks.h" -#include "test/mocks/thread_local/mocks.h" #include "test/test_common/logging.h" -#include "test/test_common/simulated_time_system.h" #include "library/common/extensions/stat_sinks/metrics_service/mobile_grpc_streamer.h" #include "library/common/extensions/stat_sinks/metrics_service/service.pb.h" diff --git a/mobile/test/common/http/BUILD b/mobile/test/common/http/BUILD index 9bb2cd5024641..faedd48ff8061 100644 --- a/mobile/test/common/http/BUILD +++ b/mobile/test/common/http/BUILD @@ -20,7 +20,6 @@ envoy_cc_test( "@envoy//test/mocks/buffer:buffer_mocks", "@envoy//test/mocks/event:event_mocks", "@envoy//test/mocks/http:api_listener_mocks", - "@envoy//test/mocks/local_info:local_info_mocks", "@envoy//test/mocks/upstream:upstream_mocks", ], ) diff --git a/mobile/test/common/http/client_test.cc b/mobile/test/common/http/client_test.cc index 0e15da98cc84b..6ec2f79b852dd 100644 --- a/mobile/test/common/http/client_test.cc +++ b/mobile/test/common/http/client_test.cc @@ -1,16 +1,22 @@ -#include +#include +#include +#include "envoy/http/header_map.h" +#include "envoy/stream_info/filter_state.h" #include "source/common/buffer/buffer_impl.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/quic/scone_state.h" #include "source/common/stats/isolated_store_impl.h" #include "test/common/http/common.h" #include "test/common/mocks/common/mocks.h" #include "test/common/mocks/event/mocks.h" #include "test/mocks/buffer/mocks.h" +#include "test/mocks/common.h" #include "test/mocks/event/mocks.h" #include "test/mocks/http/api_listener.h" #include "test/mocks/http/mocks.h" -#include "test/mocks/upstream/mocks.h" +#include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -25,10 +31,7 @@ using testing::ContainsRegex; using testing::Eq; using testing::NiceMock; using testing::Return; -using testing::ReturnPointee; using testing::ReturnRef; -using testing::SaveArg; -using testing::WithArg; namespace Envoy { namespace Http { @@ -81,6 +84,8 @@ class ClientTest : public testing::TestWithParam { }); } + void TearDown() override { http_client_.shutdownApiListener(); } + struct StreamCallbacksCalled { uint32_t on_headers_calls_{0}; uint32_t on_data_calls_{0}; @@ -151,6 +156,10 @@ class ClientTest : public testing::TestWithParam { http_client_.startStream(stream_, std::move(stream_callbacks), explicit_flow_control_ != OFF); } + Client::DirectStreamSharedPtr getDirectStream(envoy_stream_t stream_handle) { + return http_client_.getStream(stream_handle, Client::GetStreamFilters::AllowOnlyForOpenStreams); + } + void resumeDataIfEarlyResume(int32_t bytes) { if (explicit_flow_control_ == EARLY_RESUME) { auto callbacks = dynamic_cast(response_encoder_); @@ -264,7 +273,7 @@ TEST_P(ClientTest, BasicStreamData) { // test data functionality. EXPECT_CALL(dispatcher_, pushTrackedObject(_)); EXPECT_CALL(dispatcher_, popTrackedObject(_)); - EXPECT_CALL(*request_decoder_, decodeData(BufferStringEqual("request body"), true)); + EXPECT_CALL(*request_decoder_, decodeData(BufferString("request body"), true)); resumeDataIfEarlyResume(20); // Resume before data arrives. http_client_.sendData(stream_, std::move(request_data), true); resumeDataIfLateResume(20); // Resume after data arrives. @@ -349,7 +358,7 @@ TEST_P(ClientTest, MultipleDataStream) { // Send request data. EXPECT_CALL(dispatcher_, pushTrackedObject(_)); EXPECT_CALL(dispatcher_, popTrackedObject(_)); - EXPECT_CALL(*request_decoder_, decodeData(BufferStringEqual("request body1"), false)); + EXPECT_CALL(*request_decoder_, decodeData(BufferString("request body1"), false)); http_client_.sendData(stream_, std::move(request_data1), false); EXPECT_EQ(callbacks_called.on_send_window_available_calls_, 0); if (explicit_flow_control_) { @@ -362,7 +371,7 @@ TEST_P(ClientTest, MultipleDataStream) { // Send second request data. EXPECT_CALL(dispatcher_, pushTrackedObject(_)); EXPECT_CALL(dispatcher_, popTrackedObject(_)); - EXPECT_CALL(*request_decoder_, decodeData(BufferStringEqual("request body2"), true)); + EXPECT_CALL(*request_decoder_, decodeData(BufferString("request body2"), true)); http_client_.sendData(stream_, std::move(request_data2), true); // The stream is done: no further on_send_window_available calls should happen. EXPECT_EQ(callbacks_called.on_send_window_available_calls_, explicit_flow_control_ ? 1 : 0); @@ -999,5 +1008,80 @@ TEST_P(ExplicitFlowControlTest, CancelWithStreamComplete) { ASSERT_EQ(callbacks_called.on_complete_calls_, 0); } +TEST_P(ClientTest, SaveLatestStreamIntelPopulatesScone) { + StreamCallbacksCalled callbacks_called; + createStream(createDefaultStreamCallbacks(callbacks_called)); + + auto scone_state = std::make_shared(); + scone_state->scone_max_kbps = 100; + scone_state->timestamp_ms = 12345; + stream_info_.filter_state_->setData(Quic::SconeStateKey, scone_state, + StreamInfo::FilterState::LifeSpan::Connection); + + auto stream_ptr = getDirectStream(stream_); + ASSERT_NE(stream_ptr, nullptr); + + stream_ptr->saveLatestStreamIntel(); + + EXPECT_EQ(stream_ptr->stream_intel_.scone_max_kbps, 100); + EXPECT_EQ(stream_ptr->stream_intel_.scone_timestamp_ms, 12345); + EXPECT_TRUE(scone_state->scone_max_kbps.has_value()); + EXPECT_TRUE(scone_state->timestamp_ms.has_value()); +} + +TEST_P(ClientTest, SaveLatestStreamIntelPersistsScone) { + StreamCallbacksCalled callbacks_called; + createStream(createDefaultStreamCallbacks(callbacks_called)); + + auto scone_state = std::make_shared(); + scone_state->scone_max_kbps = 100; + scone_state->timestamp_ms = 12345; + stream_info_.filter_state_->setData(Quic::SconeStateKey, scone_state, + StreamInfo::FilterState::LifeSpan::Connection); + + auto stream_ptr = getDirectStream(stream_); + ASSERT_NE(stream_ptr, nullptr); + + stream_ptr->saveLatestStreamIntel(); + EXPECT_EQ(stream_ptr->stream_intel_.scone_max_kbps, 100); + EXPECT_EQ(stream_ptr->stream_intel_.scone_timestamp_ms, 12345); + + stream_ptr->saveLatestStreamIntel(); + EXPECT_EQ(stream_ptr->stream_intel_.scone_max_kbps, 100); + EXPECT_EQ(stream_ptr->stream_intel_.scone_timestamp_ms, 12345); +} + +TEST_P(ClientTest, SaveLatestStreamIntelWithNoSconeData) { + StreamCallbacksCalled callbacks_called; + createStream(createDefaultStreamCallbacks(callbacks_called)); + + auto stream_ptr = getDirectStream(stream_); + ASSERT_NE(stream_ptr, nullptr); + + stream_ptr->saveLatestStreamIntel(); + EXPECT_EQ(stream_ptr->stream_intel_.scone_max_kbps, -1); + EXPECT_EQ(stream_ptr->stream_intel_.scone_timestamp_ms, -1); +} + +TEST_P(ClientTest, SaveLatestStreamIntelWithZeroSconeValue) { + StreamCallbacksCalled callbacks_called; + createStream(createDefaultStreamCallbacks(callbacks_called)); + + auto scone_state = std::make_shared(); + scone_state->scone_max_kbps = 0; + scone_state->timestamp_ms = 12345; + stream_info_.filter_state_->setData(Quic::SconeStateKey, scone_state, + StreamInfo::FilterState::LifeSpan::Connection); + + auto stream_ptr = getDirectStream(stream_); + ASSERT_NE(stream_ptr, nullptr); + + stream_ptr->saveLatestStreamIntel(); + EXPECT_EQ(stream_ptr->stream_intel_.scone_max_kbps, 0); + EXPECT_EQ(stream_ptr->stream_intel_.scone_timestamp_ms, 12345); + EXPECT_TRUE(scone_state->scone_max_kbps.has_value()); + EXPECT_TRUE(scone_state->timestamp_ms.has_value()); +} + } // namespace Http } // namespace Envoy diff --git a/mobile/test/common/http/filters/assertion/assertion_filter_test.cc b/mobile/test/common/http/filters/assertion/assertion_filter_test.cc index be8f4b6721f29..888ccce2cb82a 100644 --- a/mobile/test/common/http/filters/assertion/assertion_filter_test.cc +++ b/mobile/test/common/http/filters/assertion/assertion_filter_test.cc @@ -21,7 +21,7 @@ class AssertionFilterTest : public testing::Test { public: void setUpFilter(std::string&& proto_str) { envoymobile::extensions::filters::http::assertion::Assertion config; - Protobuf::TextFormat::ParseFromString(proto_str, &config); + std::ignore = Protobuf::TextFormat::ParseFromString(proto_str, &config); config_ = std::make_shared(config, context_); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); @@ -689,7 +689,7 @@ match_config { } )EOF"; - Protobuf::TextFormat::ParseFromString(config_str, &proto_config); + std::ignore = Protobuf::TextFormat::ParseFromString(config_str, &proto_config); Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(proto_config, "test", context).value(); diff --git a/mobile/test/common/http/filters/assertion/filter.cc b/mobile/test/common/http/filters/assertion/filter.cc index 919f59e50a25c..9fc498f7bf745 100644 --- a/mobile/test/common/http/filters/assertion/filter.cc +++ b/mobile/test/common/http/filters/assertion/filter.cc @@ -38,7 +38,7 @@ Http::FilterHeadersStatus AssertionFilter::decodeHeaders(Http::RequestHeaderMap& if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Headers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } @@ -50,7 +50,7 @@ Http::FilterHeadersStatus AssertionFilter::decodeHeaders(Http::RequestHeaderMap& if (!final_match_status.matches_ && !final_match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } @@ -60,7 +60,7 @@ Http::FilterHeadersStatus AssertionFilter::decodeHeaders(Http::RequestHeaderMap& if (!final_match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } } @@ -74,7 +74,7 @@ Http::FilterDataStatus AssertionFilter::decodeData(Buffer::Instance& data, bool if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } @@ -86,7 +86,7 @@ Http::FilterDataStatus AssertionFilter::decodeData(Buffer::Instance& data, bool if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } @@ -96,7 +96,7 @@ Http::FilterDataStatus AssertionFilter::decodeData(Buffer::Instance& data, bool if (!match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } } @@ -109,7 +109,7 @@ Http::FilterTrailersStatus AssertionFilter::decodeTrailers(Http::RequestTrailerM if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterTrailersStatus::StopIteration; } @@ -119,7 +119,7 @@ Http::FilterTrailersStatus AssertionFilter::decodeTrailers(Http::RequestTrailerM if (!match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "Request Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterTrailersStatus::StopIteration; } return Http::FilterTrailersStatus::Continue; @@ -132,7 +132,7 @@ Http::FilterHeadersStatus AssertionFilter::encodeHeaders(Http::ResponseHeaderMap if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Headers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } @@ -144,7 +144,7 @@ Http::FilterHeadersStatus AssertionFilter::encodeHeaders(Http::ResponseHeaderMap if (!final_match_status.matches_ && !final_match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } @@ -154,7 +154,7 @@ Http::FilterHeadersStatus AssertionFilter::encodeHeaders(Http::ResponseHeaderMap if (!final_match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } } @@ -168,7 +168,7 @@ Http::FilterDataStatus AssertionFilter::encodeData(Buffer::Instance& data, bool if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } @@ -180,7 +180,7 @@ Http::FilterDataStatus AssertionFilter::encodeData(Buffer::Instance& data, bool if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } @@ -190,7 +190,7 @@ Http::FilterDataStatus AssertionFilter::encodeData(Buffer::Instance& data, bool if (!match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterDataStatus::StopIterationNoBuffer; } } @@ -203,7 +203,7 @@ Http::FilterTrailersStatus AssertionFilter::encodeTrailers(Http::ResponseTrailer if (!match_status.matches_ && !match_status.might_change_status_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Trailers do not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterTrailersStatus::StopIteration; } @@ -213,7 +213,7 @@ Http::FilterTrailersStatus AssertionFilter::encodeTrailers(Http::ResponseTrailer if (!match_status.matches_) { decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, "Response Body does not match configured expectations", - nullptr, absl::nullopt, ""); + nullptr, std::nullopt, ""); return Http::FilterTrailersStatus::StopIteration; } return Http::FilterTrailersStatus::Continue; diff --git a/mobile/test/common/http/filters/test_read/filter.cc b/mobile/test/common/http/filters/test_read/filter.cc index 27bfeeec3474c..8f0d285b927fa 100644 --- a/mobile/test/common/http/filters/test_read/filter.cc +++ b/mobile/test/common/http/filters/test_read/filter.cc @@ -26,7 +26,7 @@ Http::FilterHeadersStatus TestReadFilter::decodeHeaders(Http::RequestHeaderMap& // trigger the error and stop iteration to other filters decoder_callbacks_->sendLocalReply(Http::Code::BadRequest, "test_read filter threw: ", nullptr, - absl::nullopt, ""); + std::nullopt, ""); return Http::FilterHeadersStatus::StopIteration; } diff --git a/mobile/test/common/integration/BUILD b/mobile/test/common/integration/BUILD index 4e96a1b6c888c..1db132b65adb1 100644 --- a/mobile/test/common/integration/BUILD +++ b/mobile/test/common/integration/BUILD @@ -1,17 +1,21 @@ load( "@envoy//bazel:envoy_build_system.bzl", - "envoy_cc_test", "envoy_cc_test_library", "envoy_mobile_package", "envoy_select_envoy_mobile_xds", "envoy_select_signal_trace", ) +load( + "//bazel:envoy_mobile_cc_test.bzl", + "envoy_cc_test_library_with_engine_builder", + "envoy_cc_test_with_engine_builder", +) licenses(["notice"]) # Apache 2 envoy_mobile_package() -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "client_integration_test", srcs = ["client_integration_test.cc"], linkopts = select({ @@ -36,14 +40,16 @@ envoy_cc_test( "@envoy//source/common/quic:udp_gso_batch_writer_lib", "@envoy//source/extensions/udp_packet_writer/gso:config", "@envoy//test/extensions/filters/http/dynamic_forward_proxy:test_resolver_lib", + "@envoy//test/test_common:registry_lib", "@envoy//test/test_common:test_random_generator_lib", "@envoy//test/test_common:threadsafe_singleton_injector_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_build_config//:test_extensions", + "@quiche//:quic_test_tools_test_utils_lib", ], ) -envoy_cc_test_library( +envoy_cc_test_library_with_engine_builder( name = "base_client_integration_test_lib", srcs = [ "base_client_integration_test.cc", @@ -53,7 +59,6 @@ envoy_cc_test_library( ], repository = "@envoy", deps = [ - "//library/cc:engine_builder_lib", "//library/common/http:client_lib", "//library/common/http:header_utility_lib", "//library/common/network:network_types_lib", @@ -107,7 +112,7 @@ envoy_cc_test_library( ], ) -envoy_cc_test_library( +envoy_cc_test_library_with_engine_builder( name = "engine_with_test_server", srcs = [ "engine_with_test_server.cc", @@ -118,11 +123,10 @@ envoy_cc_test_library( repository = "@envoy", deps = [ ":test_server_lib", - "//library/cc:engine_builder_lib", ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "rtds_integration_test", srcs = envoy_select_envoy_mobile_xds( ["rtds_integration_test.cc"], @@ -137,6 +141,7 @@ envoy_cc_test( repository = "@envoy", deps = [ ":xds_integration_test_lib", + "//library/common/api:external_api_lib", "@envoy//test/test_common:environment_lib", "@envoy//test/test_common:test_runtime_lib", "@envoy//test/test_common:utility_lib", @@ -145,7 +150,7 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "cds_integration_test", srcs = envoy_select_envoy_mobile_xds( ["cds_integration_test.cc"], @@ -167,7 +172,7 @@ envoy_cc_test( ], ) -envoy_cc_test( +envoy_cc_test_with_engine_builder( name = "sds_integration_test", srcs = envoy_select_envoy_mobile_xds( ["sds_integration_test.cc"], @@ -192,7 +197,7 @@ envoy_cc_test( ], ) -envoy_cc_test_library( +envoy_cc_test_library_with_engine_builder( name = "xds_integration_test_lib", srcs = [ "xds_integration_test.cc", @@ -223,7 +228,6 @@ envoy_cc_test_library( ], repository = "@envoy", deps = [ - ":base_client_integration_test_lib", "@envoy//source/common/event:libevent_lib", "@envoy//source/common/tls:context_config_lib", "@envoy//source/common/tls:context_lib", diff --git a/mobile/test/common/integration/base_client_integration_test.cc b/mobile/test/common/integration/base_client_integration_test.cc index 7efee9274154e..8d4f5dbf8c27b 100644 --- a/mobile/test/common/integration/base_client_integration_test.cc +++ b/mobile/test/common/integration/base_client_integration_test.cc @@ -187,18 +187,22 @@ uint64_t BaseClientIntegrationTest::getCounterValue(const std::string& name) { return counter_value; } -testing::AssertionResult BaseClientIntegrationTest::waitForCounterGe(const std::string& name, - uint64_t value) { +testing::AssertionResult +BaseClientIntegrationTest::waitForCounter(const std::string& name, + testing::Matcher value_matcher) { constexpr std::chrono::milliseconds timeout = TestUtility::DefaultTimeout; Event::TestTimeSystem::RealTimeBound bound(timeout); - while (getCounterValue(name) < value) { + while (true) { + const uint64_t current_value = getCounterValue(name); + if (value_matcher.Matches(current_value)) { + return testing::AssertionSuccess(); + } timeSystem().advanceTimeWait(std::chrono::milliseconds(10)); if (timeout != std::chrono::milliseconds::zero() && !bound.withinBound()) { - return testing::AssertionFailure() - << fmt::format("timed out waiting for {} to be {}", name, value); + return testing::AssertionFailure() << "timed out waiting for " << name << " to " + << value_matcher << ": current value " << current_value; } } - return testing::AssertionSuccess(); } uint64_t BaseClientIntegrationTest::getGaugeValue(const std::string& name) { @@ -216,18 +220,22 @@ uint64_t BaseClientIntegrationTest::getGaugeValue(const std::string& name) { return gauge_value; } -testing::AssertionResult BaseClientIntegrationTest::waitForGaugeGe(const std::string& name, - uint64_t value) { +testing::AssertionResult +BaseClientIntegrationTest::waitForGauge(const std::string& name, + testing::Matcher value_matcher) { constexpr std::chrono::milliseconds timeout = TestUtility::DefaultTimeout; Event::TestTimeSystem::RealTimeBound bound(timeout); - while (getGaugeValue(name) < value) { + while (true) { + const uint64_t current_value = getGaugeValue(name); + if (value_matcher.Matches(current_value)) { + return testing::AssertionSuccess(); + } timeSystem().advanceTimeWait(std::chrono::milliseconds(10)); if (timeout != std::chrono::milliseconds::zero() && !bound.withinBound()) { - return testing::AssertionFailure() - << fmt::format("timed out waiting for {} to be {}", name, value); + return testing::AssertionFailure() << "timed out waiting for " << name << " to " + << value_matcher << ": current value " << current_value; } } - return testing::AssertionSuccess(); } } // namespace Envoy diff --git a/mobile/test/common/integration/base_client_integration_test.h b/mobile/test/common/integration/base_client_integration_test.h index 7f83fb135ecb9..9131fe8ea9bdc 100644 --- a/mobile/test/common/integration/base_client_integration_test.h +++ b/mobile/test/common/integration/base_client_integration_test.h @@ -2,7 +2,7 @@ #include "test/integration/integration.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/cc/stream.h" #include "library/cc/stream_prototype.h" #include "library/common/http/client.h" @@ -58,12 +58,12 @@ class BaseClientIntegrationTest : public BaseIntegrationTest { // Get the value of a Counter in the Envoy instance. uint64_t getCounterValue(const std::string& name); - // Wait until the Counter specified by `name` is >= `value`. - ABSL_MUST_USE_RESULT testing::AssertionResult waitForCounterGe(const std::string& name, - uint64_t value); + // Wait until the Counter specified by `name` matches `value_matcher`. + ABSL_MUST_USE_RESULT testing::AssertionResult + waitForCounter(const std::string& name, testing::Matcher value_matcher); uint64_t getGaugeValue(const std::string& name); - ABSL_MUST_USE_RESULT testing::AssertionResult waitForGaugeGe(const std::string& name, - uint64_t value); + ABSL_MUST_USE_RESULT testing::AssertionResult + waitForGauge(const std::string& name, testing::Matcher value_matcher); EnvoyStreamCallbacks createDefaultStreamCallbacks(); diff --git a/mobile/test/common/integration/cds_integration_test.cc b/mobile/test/common/integration/cds_integration_test.cc index 44da4227d1bf4..4e5cad38cd1c3 100644 --- a/mobile/test/common/integration/cds_integration_test.cc +++ b/mobile/test/common/integration/cds_integration_test.cc @@ -11,6 +11,7 @@ namespace Envoy { namespace { using envoy::config::cluster::v3::Cluster; +using testing::Ge; class CdsIntegrationTest : public XdsIntegrationTest { public: @@ -72,15 +73,15 @@ class CdsIntegrationTest : public XdsIntegrationTest { {cluster}, {cluster}, {}, version); // Wait for cluster to be added. - EXPECT_TRUE(waitForCounterGe("cluster_manager.cluster_added", 1)); - EXPECT_TRUE(waitForGaugeGe("cluster_manager.active_clusters", cluster_count + 1)); + EXPECT_TRUE(waitForCounter("cluster_manager.cluster_added", Ge(1))); + EXPECT_TRUE(waitForGauge("cluster_manager.active_clusters", Ge(cluster_count + 1))); // ACK of the initial version. EXPECT_TRUE(compareDiscoveryRequest(Config::getTypeUrl(), version, expected_resources, {}, {}, /*expect_node=*/false)); - EXPECT_TRUE(waitForGaugeGe("cluster_manager.cluster_removed", 0)); + EXPECT_TRUE(waitForGauge("cluster_manager.cluster_removed", Ge(0))); } void sendUpdatedCdsResponseAndVerify(const std::string& version) { @@ -98,8 +99,8 @@ class CdsIntegrationTest : public XdsIntegrationTest { /*expect_node=*/false)); // Cluster count should stay the same. - EXPECT_TRUE(waitForGaugeGe("cluster_manager.active_clusters", cluster_count)); - EXPECT_TRUE(waitForGaugeGe("cluster_manager.cluster_removed", 0)); + EXPECT_TRUE(waitForGauge("cluster_manager.active_clusters", Ge(cluster_count))); + EXPECT_TRUE(waitForGauge("cluster_manager.cluster_removed", Ge(0))); } bool use_xdstp_{false}; diff --git a/mobile/test/common/integration/client_integration_test.cc b/mobile/test/common/integration/client_integration_test.cc index 2f97e469458c5..13d4f8aa62320 100644 --- a/mobile/test/common/integration/client_integration_test.cc +++ b/mobile/test/common/integration/client_integration_test.cc @@ -18,6 +18,7 @@ #include "test/extensions/filters/http/dynamic_forward_proxy/test_resolver.h" #include "test/integration/autonomous_upstream.h" #include "test/test_common/registry.h" +#include "source/common/common/logger.h" #include "test/test_common/test_random_generator.h" #include "test/test_common/threadsafe_singleton_injector.h" @@ -29,12 +30,13 @@ #include "library/common/network/network_types.h" #include "library/common/network/proxy_settings.h" #include "library/common/types/c_types.h" +#include "quiche/quic/test_tools/quic_test_utils.h" using testing::_; using testing::AnyNumber; +using testing::Ge; using testing::Return; using testing::ReturnRef; - namespace Envoy { namespace { @@ -44,14 +46,14 @@ namespace { // www.lyft.com -> fake test upstream. class TestKeyValueStore : public Envoy::Platform::KeyValueStore { public: - absl::optional read(const std::string&) override { + std::optional read(const std::string&) override { ASSERT(!value_.empty()); return value_; } void save(std::string, std::string) override {} void remove(const std::string&) override {} - void addOrUpdate(absl::string_view, absl::string_view, absl::optional) {} - absl::optional get(absl::string_view) { return {}; } + void addOrUpdate(absl::string_view, absl::string_view, std::optional) {} + std::optional get(absl::string_view) { return {}; } void flush() {} void iterate(::Envoy::KeyValueStore::ConstIterateCb) const {} void setValue(std::string value) { value_ = value; } @@ -60,14 +62,15 @@ class TestKeyValueStore : public Envoy::Platform::KeyValueStore { std::string value_; }; -class ClientIntegrationTest - : public BaseClientIntegrationTest, - public testing::TestWithParam> { +class ClientIntegrationTest : public BaseClientIntegrationTest, + public testing::TestWithParam< + std::tuple> { public: static void SetUpTestCase() { test_key_value_store_ = std::make_shared(); } static void TearDownTestCase() { test_key_value_store_.reset(); } Http::CodecType getCodecType() { return std::get<1>(GetParam()); } + bool getUseWorkerThread() { return std::get<2>(GetParam()); } ClientIntegrationTest() : BaseClientIntegrationTest(/*ip_version=*/std::get<0>(GetParam())) { // For server TLS @@ -84,13 +87,23 @@ class ClientIntegrationTest Extensions::TransportSockets::Tls::forceRegisterDefaultCertValidatorFactory(); } + ~ClientIntegrationTest() override { Logger::Context::changeAllLogLevels(spdlog::level::info); } + void initialize() override { + builder_.setLogLevel(log_level_); + Logger::Context::changeAllLogLevels(static_cast(log_level_)); + builder_.enableWorkerThread(getUseWorkerThread()); + if (getUseWorkerThread()) { + // Platform cert validation is disabled when using worker thread. The engine will use the + // built-in root certs which doesn't have the CA cert for our fake upstream. + builder_.enforceTrustChainVerification(false); + } // Integration test starts upstreams before Envoy which can cause a data race. builder_.enableLogger(false); - builder_.setLogLevel(Logger::Logger::trace); builder_.addRuntimeGuard("dns_cache_set_ip_version_to_remove", true); builder_.addRuntimeGuard("quic_no_tcp_delay", true); builder_.addRuntimeGuard("mobile_use_network_observer_registry", true); + builder_.addRuntimeGuard("getaddrinfo_no_ai_flags", true); if (getCodecType() == Http::CodecType::HTTP3) { setUpstreamProtocol(Http::CodecType::HTTP3); @@ -193,16 +206,18 @@ class ClientIntegrationTest } static std::string testParamsToString( - const testing::TestParamInfo> + const testing::TestParamInfo> params) { return fmt::format( - "{}_{}", + "{}_{}_{}", TestUtility::ipTestParamsToString(testing::TestParamInfo( std::get<0>(params.param), params.index)), - protocolToString(std::get<1>(params.param))); + protocolToString(std::get<1>(params.param)), + std::get<2>(params.param) ? "WorkerThreadOn" : "WorkerThreadOff"); } protected: + Logger::Logger::Levels log_level_ = Logger::Logger::info; std::unique_ptr helper_handle_; bool add_quic_hints_ = false; bool add_fake_dns_ = false; @@ -217,14 +232,17 @@ INSTANTIATE_TEST_SUITE_P( IpVersions, ClientIntegrationTest, testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), testing::ValuesIn({Http::CodecType::HTTP1, Http::CodecType::HTTP2, - Http::CodecType::HTTP3})), + Http::CodecType::HTTP3}), + testing::Bool()), ClientIntegrationTest::testParamsToString); void ClientIntegrationTest::basicTest() { if (getCodecType() != Http::CodecType::HTTP1) { EXPECT_CALL(helper_handle_->mock_helper(), isCleartextPermitted(_)).Times(0); - EXPECT_CALL(helper_handle_->mock_helper(), validateCertificateChain(_, _)); - EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()); + if (!getUseWorkerThread()) { + EXPECT_CALL(helper_handle_->mock_helper(), validateCertificateChain(_, _)); + EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()); + } } Buffer::OwnedImpl request_data = Buffer::OwnedImpl("request body"); default_request_headers_.addCopy(AutonomousStream::EXPECT_REQUEST_SIZE_BYTES, @@ -291,10 +309,11 @@ TEST_P(ClientIntegrationTest, DisableDnsRefreshOnFailure) { dns_resolver_config.set_name("envoy.test.mock_dns_resolver"); envoy::test::mock_dns_resolver::v3::MockDnsResolverConfig config; config.add_non_existent_domains("doesnotexist"); - dns_resolver_config.mutable_typed_config()->PackFrom(config); + std::ignore = dns_resolver_config.mutable_typed_config()->PackFrom(config); builder_.setDnsResolver(dns_resolver_config); builder_.setDisableDnsRefreshOnFailure(true); + log_level_ = Logger::Logger::debug; initialize(); default_request_headers_.setHost("doesnotexist"); @@ -321,6 +340,7 @@ TEST_P(ClientIntegrationTest, DisableDnsRefreshOnNetworkChange) { } }); builder_.setDisableDnsRefreshOnNetworkChange(true); + log_level_ = Logger::Logger::debug; initialize(); internalEngine()->onDefaultNetworkChanged(1); @@ -341,6 +361,7 @@ TEST_P(ClientIntegrationTest, HandleNetworkChangeEvents) { } }); builder_.setDisableDnsRefreshOnNetworkChange(false); + log_level_ = Logger::Logger::trace; initialize(); // Set the network type to WIFI. This should trigger a network change. @@ -401,7 +422,7 @@ TEST_P(ClientIntegrationTest, HandleNetworkChangeEventsAndroid) { } }); builder_.setDisableDnsRefreshOnNetworkChange(false); - + log_level_ = Logger::Logger::trace; initialize(); // A new WIFI network appears and becomes the default network. Even though @@ -416,6 +437,7 @@ TEST_P(ClientIntegrationTest, HandleNetworkChangeEventsAndroid) { } TEST_P(ClientIntegrationTest, Http3IdleConnectionClosedUponNetworkChangeEventsAndroid) { + expect_data_streams_ = false; builder_.enableQuicConnectionMigration(true); builder_.addRuntimeGuard("decouple_explicit_drain_pools_and_dns_refresh", true); builder_.addRuntimeGuard("mobile_use_network_observer_registry", true); @@ -454,17 +476,17 @@ TEST_P(ClientIntegrationTest, Http3IdleConnectionClosedUponNetworkChangeEventsAn ASSERT_EQ(0, last_stream_final_intel_.socket_reused); // An h3 upstream connection should have been established. - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_cx_http3_total", 1)); - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_rq_total", 1)); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_cx_http3_total", Ge(1))); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_rq_total", Ge(1))); EXPECT_CALL(helper_handle_->mock_helper(), bindSocketToNetwork(_, 123)).Times(0u); // A new cellular network appears and becomes the default network. The idle connection should be // closed. internalEngine()->onNetworkConnectAndroid(ConnectionType::CONNECTION_4G, 123); internalEngine()->onDefaultNetworkChangedAndroid(ConnectionType::CONNECTION_4G, 123); - ASSERT_TRUE(waitForCounterGe("http3.upstream.tx.quic_connection_close_error_code_QUIC_CONNECTION_" - "MIGRATION_NO_MIGRATABLE_STREAMS", - 1)); + ASSERT_TRUE(waitForCounter("http3.upstream.tx.quic_connection_close_error_code_QUIC_CONNECTION_" + "MIGRATION_NO_MIGRATABLE_STREAMS", + Ge(1))); // A new connection will be created on the new default network to serve new requests. EXPECT_CALL(helper_handle_->mock_helper(), bindSocketToNetwork(_, 123)) @@ -492,7 +514,7 @@ TEST_P(ClientIntegrationTest, Http3IdleConnectionClosedUponNetworkChangeEventsAn ASSERT_EQ(3, last_stream_final_intel_.upstream_protocol); ASSERT_EQ(0, last_stream_final_intel_.socket_reused); - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_rq_total", 2)); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_rq_total", Ge(2))); // The total h3 connection count should have increased. EXPECT_EQ(2, getCounterValue("cluster.base.upstream_cx_http3_total")); } @@ -501,6 +523,7 @@ TEST_P(ClientIntegrationTest, Http3ConnectionMigrationUponNetworkChangeEventsAnd builder_.enableQuicConnectionMigration(true); builder_.addRuntimeGuard("decouple_explicit_drain_pools_and_dns_refresh", true); builder_.addRuntimeGuard("mobile_use_network_observer_registry", true); + builder_.setMigrateIdleQuicConnection(true); initialize(); if (getCodecType() != Http::CodecType::HTTP3 || version_ != Network::Address::IpVersion::v4) { @@ -520,8 +543,8 @@ TEST_P(ClientIntegrationTest, Http3ConnectionMigrationUponNetworkChangeEventsAnd stream_->sendHeaders(std::make_unique(default_request_headers_), false); // Wait for the upstream connection to be established. - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_cx_http3_total", 1)); - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_rq_total", 1)); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_cx_http3_total", Ge(1))); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_rq_total", Ge(1))); absl::Notification probing_socket_created; EXPECT_CALL(helper_handle_->mock_helper(), bindSocketToNetwork(_, 123)) @@ -572,7 +595,7 @@ TEST_P(ClientIntegrationTest, Http3ConnectionMigrationUponNetworkChangeEventsAnd ASSERT_EQ(3, last_stream_final_intel_.upstream_protocol); ASSERT_EQ(1, last_stream_final_intel_.socket_reused); - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_rq_total", 2)); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_rq_total", Ge(2))); // The total h3 connection count shouldn't have increased. EXPECT_EQ(1, getCounterValue("cluster.base.upstream_cx_http3_total")); } @@ -608,8 +631,8 @@ TEST_P(ClientIntegrationTest, Http3ConnectionMigrationUponNetworkDisconnectedAnd ASSERT_EQ(0, last_stream_final_intel_.socket_reused); // Wait for the upstream connection to be established. - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_cx_http3_total", 1)); - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_rq_total", 1)); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_cx_http3_total", Ge(1))); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_rq_total", Ge(1))); // Send a new request with body during which network gets disconnected. Buffer::OwnedImpl request_data = Buffer::OwnedImpl("request body"); @@ -656,9 +679,9 @@ TEST_P(ClientIntegrationTest, Http3ConnectionMigrationUponNetworkDisconnectedAnd internalEngine()->onNetworkConnectAndroid(ConnectionType::CONNECTION_WIFI, 1); internalEngine()->onDefaultNetworkChangedAndroid(ConnectionType::CONNECTION_WIFI, 1); - ASSERT_TRUE(waitForCounterGe("http3.upstream.tx.quic_connection_close_error_code_QUIC_CONNECTION_" - "MIGRATION_NO_MIGRATABLE_STREAMS", - 1)); + ASSERT_TRUE(waitForCounter("http3.upstream.tx.quic_connection_close_error_code_QUIC_CONNECTION_" + "MIGRATION_NO_MIGRATABLE_STREAMS", + Ge(1))); } TEST_P(ClientIntegrationTest, LargeResponse) { @@ -959,8 +982,10 @@ TEST_P(ClientIntegrationTest, ClearTextNotPermitted) { TEST_P(ClientIntegrationTest, BasicHttps) { EXPECT_CALL(helper_handle_->mock_helper(), isCleartextPermitted(_)).Times(0); - EXPECT_CALL(helper_handle_->mock_helper(), validateCertificateChain(_, _)); - EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()); + if (!getUseWorkerThread()) { + EXPECT_CALL(helper_handle_->mock_helper(), validateCertificateChain(_, _)); + EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()); + } builder_.enablePlatformCertificatesValidation(true); @@ -1030,7 +1055,7 @@ TEST_P(ClientIntegrationTest, InvalidDomain) { dns_resolver_config.set_name("envoy.test.mock_dns_resolver"); envoy::test::mock_dns_resolver::v3::MockDnsResolverConfig config; config.add_non_existent_domains("www.doesnotexist.com"); - dns_resolver_config.mutable_typed_config()->PackFrom(config); + std::ignore = dns_resolver_config.mutable_typed_config()->PackFrom(config); builder_.setDnsResolver(dns_resolver_config); initialize(); @@ -1086,7 +1111,7 @@ TEST_P(ClientIntegrationTest, InvalidDomainReresolveWithNoAddresses) { dns_resolver_config.set_name("envoy.test.mock_dns_resolver"); envoy::test::mock_dns_resolver::v3::MockDnsResolverConfig config; config.add_non_existent_domains("www.doesnotexist.com"); - dns_resolver_config.mutable_typed_config()->PackFrom(config); + std::ignore = dns_resolver_config.mutable_typed_config()->PackFrom(config); builder_.setDnsResolver(dns_resolver_config); initialize(); @@ -1108,7 +1133,10 @@ TEST_P(ClientIntegrationTest, InvalidDomainReresolveWithNoAddresses) { } TEST_P(ClientIntegrationTest, ReresolveAndDrain) { + expect_data_streams_ = false; // 0-RTT request might skip some fields in final stream intel. builder_.enableDrainPostDnsRefresh(true); + // 0-RTT requests will not populate some of the final stream intel fields, so skip the validation. + expect_data_streams_ = false; add_fake_dns_ = true; Network::OverrideAddrInfoDnsResolverFactory factory; Registry::InjectFactory inject_factory(factory); @@ -1154,7 +1182,7 @@ TEST_P(ClientIntegrationTest, ReresolveAndDrain) { } // Make sure the attempt happened. - ASSERT_TRUE(waitForCounterGe("dns_cache.base_dns_cache.dns_query_attempt", 1)); + ASSERT_TRUE(waitForCounter("dns_cache.base_dns_cache.dns_query_attempt", Ge(1))); EXPECT_EQ(0, getCounterValue("dns_cache.base_dns_cache.dns_query_success")); // The next request should go to the original upstream as there's been no drain. stream_ = createNewStream(createDefaultStreamCallbacks()); @@ -1168,7 +1196,7 @@ TEST_P(ClientIntegrationTest, ReresolveAndDrain) { // Force the lookup to resolve to localhost. // Unblock the resolution and wait for it to succeed. Network::TestResolver::unblockResolve("127.0.0.3"); - ASSERT_TRUE(waitForCounterGe("dns_cache.base_dns_cache.dns_query_success", 1)); + ASSERT_TRUE(waitForCounter("dns_cache.base_dns_cache.dns_query_success", Ge(1))); // Do one final request. It should go to the second upstream and return 202 stream_ = createNewStream(createDefaultStreamCallbacks()); @@ -1417,9 +1445,9 @@ TEST_P(ClientIntegrationTest, CancelDuringResponse) { ASSERT_TRUE(upstream_connection_->waitForDisconnect()); upstream_connection_.reset(); ASSERT_TRUE( - waitForCounterGe("http3.upstream.tx.quic_connection_close_error_code_QUIC_NO_ERROR", 1)); - ASSERT_TRUE(waitForCounterGe( - "http3.upstream.tx.quic_reset_stream_error_code_QUIC_STREAM_REQUEST_REJECTED", 1)); + waitForCounter("http3.upstream.tx.quic_connection_close_error_code_QUIC_NO_ERROR", Ge(1))); + ASSERT_TRUE(waitForCounter( + "http3.upstream.tx.quic_reset_stream_error_code_QUIC_STREAM_REQUEST_REJECTED", Ge(1))); } } @@ -1681,7 +1709,7 @@ TEST_P(ClientIntegrationTest, ResetWithBidiTrafficExplicitData) { } TEST_P(ClientIntegrationTest, Proxying) { - if (getCodecType() != Http::CodecType::HTTP1) { + if (getCodecType() != Http::CodecType::HTTP1 || getUseWorkerThread()) { return; } initialize(); @@ -1725,12 +1753,15 @@ TEST_P(ClientIntegrationTest, TestStats) { { absl::MutexLock l(engine_lock_); std::string stats = engine_->dumpStats(); - EXPECT_TRUE((absl::StrContains(stats, "runtime.load_success: 1"))) << stats; + EXPECT_TRUE((absl::StrContains(stats, "runtime.load_success: 2"))) << stats; } } #if defined(__APPLE__) TEST_P(ClientIntegrationTest, TestProxyResolutionApi) { + if (getUseWorkerThread()) { + return; + } builder_.respectSystemProxySettings(true); initialize(); ASSERT_TRUE(Envoy::Api::External::retrieveApi("envoy_proxy_resolver") != nullptr); @@ -1741,6 +1772,9 @@ TEST_P(ClientIntegrationTest, TestProxyResolutionApi) { // doesn't crash. It doesn't really test the actual network change event. TEST_P(ClientIntegrationTest, OnNetworkChanged) { builder_.addRuntimeGuard("dns_cache_set_ip_version_to_remove", true); + if (getCodecType() == Http::CodecType::HTTP3) { + builder_.setDisableDnsRefreshOnNetworkChange(true); + } initialize(); internalEngine()->onDefaultNetworkChanged(1); basicTest(); @@ -1753,6 +1787,9 @@ TEST_P(ClientIntegrationTest, OnNetworkChanged) { // doesn't crash. It doesn't really test the actual network change event. TEST_P(ClientIntegrationTest, OnNetworkChangeEvent) { builder_.addRuntimeGuard("dns_cache_set_ip_version_to_remove", true); + if (getCodecType() == Http::CodecType::HTTP3) { + builder_.setDisableDnsRefreshOnNetworkChange(true); + } initialize(); int network = 0; network |= static_cast(NetworkType::WWAN); @@ -1764,15 +1801,36 @@ TEST_P(ClientIntegrationTest, OnNetworkChangeEvent) { } } +// A thread-safe implementation which can fake SOCKET_ERROR_NOBUFS send errors. class MockSendOsSysCalls : public Api::OsSysCallsImpl { public: + MockSendOsSysCalls() { + ON_CALL(*this, send(_, _, _, _)) + .WillByDefault([this](os_fd_t socket, void* buffer, size_t length, int flags) { + int target = target_fd_.load(); + if (fail_send_.load() && (target == -1 || socket == target)) { + bool expected = true; + if (fail_send_.compare_exchange_strong(expected, false)) { + return Api::SysCallSizeResult{-1, SOCKET_ERROR_NOBUFS}; + } + } + return Api::OsSysCallsImpl::send(socket, buffer, length, flags); + }); + } + MOCK_METHOD(Api::SysCallSizeResult, send, (os_fd_t socket, void* buffer, size_t length, int flags), (override)); + + std::atomic fail_send_{false}; + std::atomic target_fd_{-1}; }; // Tests that a transient write error due to no space available on the socket // does not cause the stream to error out when using HTTP/3. TEST_P(ClientIntegrationTest, NoSpaceAvailableWriteErrorSwallowed) { + testing::NiceMock sys_calls; + TestThreadsafeSingletonInjector injector(&sys_calls); + initialize(); if (upstreamProtocol() != Http::CodecType::HTTP3) { return; @@ -1795,14 +1853,9 @@ TEST_P(ClientIntegrationTest, NoSpaceAvailableWriteErrorSwallowed) { // Wait for the upstream connection to be created and introduce a transient SOCKET_ERROR_NOBUFS // write error. - ASSERT_TRUE(waitForCounterGe("cluster.base.upstream_cx_http3_total", 1)); - MockSendOsSysCalls sys_calls; - TestThreadsafeSingletonInjector injector(&sys_calls); - EXPECT_CALL(sys_calls, send(fd, _, _, _)) - .WillOnce(Return(Api::SysCallSizeResult{-1, SOCKET_ERROR_NOBUFS})) - .WillRepeatedly(Invoke([&](os_fd_t socket, void* buffer, size_t length, int flags) { - return injector.latched().send(socket, buffer, length, flags); - })); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_cx_http3_total", Ge(1))); + sys_calls.target_fd_.store(fd); + sys_calls.fail_send_.store(true); // Complete the request. stream_->close(Http::Utility::createRequestTrailerMapPtr()); @@ -1812,7 +1865,421 @@ TEST_P(ClientIntegrationTest, NoSpaceAvailableWriteErrorSwallowed) { ASSERT_EQ(cc_.on_error_calls_, 0); ASSERT_EQ(cc_.on_complete_calls_, 1); EXPECT_EQ(cc_.status_, "200"); + + // Join background threads before local injector and mocks are destroyed to avoid TSAN data races. + TearDown(); +} + +TEST_P(ClientIntegrationTest, HttpsWithEarlyData) { + // Dummy comment to invalidate cache + if (getCodecType() != Http::CodecType::HTTP3 || + !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fix_http3_early_data_timing")) { + return; + } + EXPECT_CALL(helper_handle_->mock_helper(), isCleartextPermitted(_)).Times(0); + EXPECT_CALL(helper_handle_->mock_helper(), validateCertificateChain(_, _)).Times(AnyNumber()); + EXPECT_CALL(helper_handle_->mock_helper(), cleanupAfterCertificateValidation()) + .Times(AnyNumber()); + + autonomous_upstream_ = false; + + initialize(); + default_request_headers_.setScheme("https"); + stream_ = createNewStream(createDefaultStreamCallbacks()); + stream_->sendHeaders(std::make_unique(default_request_headers_), + true); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + upstream_connection_)); + ASSERT_TRUE( + upstream_connection_->waitForNewStream(*BaseIntegrationTest::dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + upstream_request_->encodeData(10, true); + + terminal_callback_.waitReady(); + + ASSERT_EQ(cc_.on_headers_calls_, 1); + ASSERT_EQ(cc_.status_, "200"); + ASSERT_GE(cc_.on_data_calls_, 1); + ASSERT_EQ(cc_.on_complete_calls_, 1); + EXPECT_EQ(0, getCounterValue("cluster.base.upstream_cx_connect_with_0_rtt")); + + // Wait for session ticket to be received (QUIC sends it after handshake) + timeSystem().realSleepDoNotUseWithoutScrutiny(std::chrono::milliseconds(500)); + + int old_upstream_cx_destroy = getCounterValue("cluster.base.upstream_cx_destroy"); + // Close connection to force reconnect and use session ticket + ASSERT_TRUE(upstream_connection_->close()); + ASSERT_TRUE(upstream_connection_->waitForDisconnect()); + upstream_connection_.reset(); + ASSERT_TRUE(waitForCounter("cluster.base.upstream_cx_destroy", Ge(old_upstream_cx_destroy + 1))); + + // Reset terminal callback for the second request. + ConditionalInitializer terminal_callback; + cc_.terminal_callback_ = &terminal_callback; + // Skip validating final stream intel for the second request. + expect_data_streams_ = false; + + default_request_headers_.addCopy("second_request", "1"); + stream_ = createNewStream(createDefaultStreamCallbacks()); + stream_->sendHeaders(std::make_unique(default_request_headers_), + true); + + // Handle second request on upstream + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + upstream_connection_)); + ASSERT_TRUE( + upstream_connection_->waitForNewStream(*BaseIntegrationTest::dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + upstream_request_->encodeData(10, true); + + terminal_callback.waitReady(); + + ASSERT_EQ(cc_.on_headers_calls_, 2); + ASSERT_EQ(cc_.status_, "200"); + + EXPECT_EQ(1, getCounterValue("cluster.base.upstream_cx_connect_with_0_rtt")); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fix_http3_early_data_timing")) { + EXPECT_EQ(1, getCounterValue("cluster.base.upstream_rq_0rtt")); + } +} + +class MockRecvMsgOsSysCalls : public Api::OsSysCallsImpl { +public: + MockRecvMsgOsSysCalls() { + ON_CALL(*this, recvmsg(_, _, _)) + .WillByDefault(Invoke([this](os_fd_t sockfd, msghdr* msg, int flags) { + auto result = Api::OsSysCallsImpl::recvmsg(sockfd, msg, flags); + int16_t bandwidth = scone_bandwidth_.exchange(-1); + if (result.return_value_ <= 0 || bandwidth == -1) { + return result; + } + + std::vector new_buffer(result.return_value_); + if (quic::test::MaybeUpdateSconePacket( + reinterpret_cast(msg->msg_iov[0].iov_base), new_buffer.data(), + result.return_value_, static_cast(bandwidth))) { + memcpy(msg->msg_iov[0].iov_base, new_buffer.data(), result.return_value_); + } + return result; + })); + } + + MOCK_METHOD(Api::SysCallSizeResult, recvmsg, (os_fd_t sockfd, msghdr* msg, int flags), + (override)); + + std::atomic scone_bandwidth_{-1}; +}; + +TEST_P(ClientIntegrationTest, SconeValuePropagation) { + if (upstreamProtocol() != Http::CodecType::HTTP3) { + return; + } + + const int16_t expected_bandwidth = 127; + + MockRecvMsgOsSysCalls sys_calls; + TestThreadsafeSingletonInjector injector(&sys_calls); + + builder_.enableScone(true); + initialize(); + + int64_t captured_scone_max_kbps = -1; + int64_t captured_scone_timestamp_ms = -1; + + EnvoyStreamCallbacks stream_callbacks = createDefaultStreamCallbacks(); + stream_callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, bool, + envoy_stream_intel intel) { + cc_.on_headers_calls_++; + cc_.status_ = absl::StrCat(headers.getStatusValue()); + captured_scone_max_kbps = intel.scone_max_kbps; + captured_scone_timestamp_ms = intel.scone_timestamp_ms; + }; + + stream_ = createNewStream(std::move(stream_callbacks)); + sys_calls.scone_bandwidth_.store(expected_bandwidth); + stream_->sendHeaders(std::make_unique(default_request_headers_), + true); + + cc_.terminal_callback_->waitReady(); + + EXPECT_EQ(captured_scone_max_kbps, expected_bandwidth); + EXPECT_GT(captured_scone_timestamp_ms, 0); + + int64_t captured_scone_max_kbps2 = -1; + int64_t captured_scone_timestamp_ms2 = -1; + EnvoyStreamCallbacks stream_callbacks2 = createDefaultStreamCallbacks(); + stream_callbacks2.on_headers_ = [&](const Http::ResponseHeaderMap& headers, bool, + envoy_stream_intel intel) { + cc_.on_headers_calls_++; + cc_.status_ = absl::StrCat(headers.getStatusValue()); + captured_scone_max_kbps2 = intel.scone_max_kbps; + captured_scone_timestamp_ms2 = intel.scone_timestamp_ms; + }; + + ConditionalInitializer terminal_callback2; + cc_.terminal_callback_ = &terminal_callback2; + + auto stream2 = createNewStream(std::move(stream_callbacks2)); + stream2->sendHeaders(std::make_unique(default_request_headers_), + true); + + terminal_callback2.waitReady(); + + EXPECT_EQ(captured_scone_max_kbps2, expected_bandwidth); + EXPECT_GT(captured_scone_timestamp_ms2, 0); +} + +TEST_P(ClientIntegrationTest, SconeValuePropagationDelayed) { + if (upstreamProtocol() != Http::CodecType::HTTP3) { + return; + } + + const int16_t expected_bandwidth = 50; + + MockRecvMsgOsSysCalls sys_calls; + TestThreadsafeSingletonInjector injector(&sys_calls); + + builder_.enableScone(true); + initialize(); + + int64_t captured_scone_max_kbps_headers = -1; + int64_t captured_scone_max_kbps_data = -1; + int64_t captured_scone_timestamp_ms_headers = -1; + int64_t captured_scone_timestamp_ms_data = -1; + + absl::Notification headers_received; + absl::Notification data_received; + + EnvoyStreamCallbacks stream_callbacks = createDefaultStreamCallbacks(); + stream_callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, bool, + envoy_stream_intel intel) { + cc_.on_headers_calls_++; + cc_.status_ = absl::StrCat(headers.getStatusValue()); + captured_scone_max_kbps_headers = intel.scone_max_kbps; + captured_scone_timestamp_ms_headers = intel.scone_timestamp_ms; + if (!headers_received.HasBeenNotified()) { + headers_received.Notify(); + } + }; + stream_callbacks.on_data_ = [&](const Buffer::Instance&, uint64_t, bool, + envoy_stream_intel intel) { + cc_.on_data_calls_++; + captured_scone_max_kbps_data = intel.scone_max_kbps; + captured_scone_timestamp_ms_data = intel.scone_timestamp_ms; + if (!data_received.HasBeenNotified()) { + data_received.Notify(); + } + }; + + stream_ = createNewStream(std::move(stream_callbacks)); + stream_->sendHeaders(std::make_unique(default_request_headers_), + true); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + upstream_connection_)); + ASSERT_TRUE( + upstream_connection_->waitForNewStream(*BaseIntegrationTest::dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + + headers_received.WaitForNotification(); + + EXPECT_EQ(captured_scone_max_kbps_headers, -1); + EXPECT_EQ(captured_scone_timestamp_ms_headers, -1); + + sys_calls.scone_bandwidth_.store(expected_bandwidth); + + upstream_request_->encodeData(10, false); + + data_received.WaitForNotification(); + + EXPECT_EQ(captured_scone_max_kbps_data, expected_bandwidth); + EXPECT_GT(captured_scone_timestamp_ms_data, 0); + + upstream_request_->encodeData(0, true); + + terminal_callback_.waitReady(); +} + +TEST_P(ClientIntegrationTest, SconeValuePropagationMultipleUpdates) { + if (upstreamProtocol() != Http::CodecType::HTTP3) { + return; + } + + const int16_t expected_bandwidth_1 = 10; + const int16_t expected_bandwidth_2 = 20; + + MockRecvMsgOsSysCalls sys_calls; + TestThreadsafeSingletonInjector injector(&sys_calls); + + builder_.enableScone(true); + initialize(); + + int64_t captured_scone_max_kbps_headers = -1; + int64_t captured_scone_max_kbps_data = -1; + int64_t captured_scone_timestamp_ms_headers = -1; + int64_t captured_scone_timestamp_ms_data = -1; + + absl::Notification headers_received; + absl::Notification data_received; + + EnvoyStreamCallbacks stream_callbacks = createDefaultStreamCallbacks(); + stream_callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, bool, + envoy_stream_intel intel) { + cc_.on_headers_calls_++; + cc_.status_ = absl::StrCat(headers.getStatusValue()); + captured_scone_max_kbps_headers = intel.scone_max_kbps; + captured_scone_timestamp_ms_headers = intel.scone_timestamp_ms; + if (!headers_received.HasBeenNotified()) { + headers_received.Notify(); + } + }; + stream_callbacks.on_data_ = [&](const Buffer::Instance&, uint64_t, bool, + envoy_stream_intel intel) { + cc_.on_data_calls_++; + captured_scone_max_kbps_data = intel.scone_max_kbps; + captured_scone_timestamp_ms_data = intel.scone_timestamp_ms; + if (!data_received.HasBeenNotified()) { + data_received.Notify(); + } + }; + + stream_ = createNewStream(std::move(stream_callbacks)); + sys_calls.scone_bandwidth_.store(expected_bandwidth_1); + stream_->sendHeaders(std::make_unique(default_request_headers_), + true); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + upstream_connection_)); + ASSERT_TRUE( + upstream_connection_->waitForNewStream(*BaseIntegrationTest::dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + + headers_received.WaitForNotification(); + + EXPECT_EQ(captured_scone_max_kbps_headers, expected_bandwidth_1); + EXPECT_GT(captured_scone_timestamp_ms_headers, 0); + + sys_calls.scone_bandwidth_.store(expected_bandwidth_2); + + upstream_request_->encodeData(10, false); + + data_received.WaitForNotification(); + + EXPECT_EQ(captured_scone_max_kbps_data, expected_bandwidth_2); + EXPECT_GT(captured_scone_timestamp_ms_data, 0); + EXPECT_GE(captured_scone_timestamp_ms_data, captured_scone_timestamp_ms_headers); + + upstream_request_->encodeData(0, true); + + terminal_callback_.waitReady(); } +TEST_P(ClientIntegrationTest, DrainConnectionsBySocketTag) { + autonomous_upstream_ = false; + builder_.enableSocketTagging(true); + builder_.enableStatsCollection(true); + initialize(); + + Platform::EngineSharedPtr engine; + { + absl::MutexLock l(engine_lock_); + engine = engine_; + } + + auto send_request_with_tag = [&](int tag_value, ConditionalInitializer& terminal, + std::string& status) { + Buffer::OwnedImpl request_data = Buffer::OwnedImpl("request body"); + Http::TestRequestHeaderMapImpl request_headers = default_request_headers_; + request_headers.addCopy(Http::LowerCaseString("x-envoy-mobile-socket-tag"), + absl::StrCat("0,", tag_value)); + + EnvoyStreamCallbacks callbacks; + callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, bool, envoy_stream_intel) { + status = absl::StrCat(headers.getStatusValue()); + }; + callbacks.on_data_ = [](const Buffer::Instance&, uint64_t, bool, envoy_stream_intel) {}; + callbacks.on_complete_ = [&terminal](envoy_stream_intel, envoy_final_stream_intel) { + terminal.setReady(); + }; + callbacks.on_error_ = [&terminal](const EnvoyError&, envoy_stream_intel, + envoy_final_stream_intel) { terminal.setReady(); }; + callbacks.on_cancel_ = [&terminal](envoy_stream_intel, envoy_final_stream_intel) { + terminal.setReady(); + }; + + Platform::StreamSharedPtr stream = createNewStream(std::move(callbacks)); + stream->sendHeaders(std::make_unique(request_headers), false); + stream->sendData(std::make_unique(std::move(request_data))); + stream->close(Http::Utility::createRequestTrailerMapPtr()); + return stream; + }; + + // 1. First request to establish a connection with socket tag 12345 + ConditionalInitializer terminal1; + std::string status1; + auto s1 = send_request_with_tag(12345, terminal1, status1); + + FakeHttpConnectionPtr fake_upstream_connection1; + FakeStreamPtr fake_stream1; + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + fake_upstream_connection1)); + ASSERT_TRUE( + fake_upstream_connection1->waitForNewStream(*BaseIntegrationTest::dispatcher_, fake_stream1)); + ASSERT_TRUE(fake_stream1->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + fake_stream1->encodeHeaders(Http::TestResponseHeaderMapImpl({{":status", "200"}}), false); + fake_stream1->encodeData(100, true); + terminal1.waitReady(); + ASSERT_EQ(status1, "200"); + + // 2. Second request to the same host with a different socket tag 67890 + ConditionalInitializer terminal2; + std::string status2; + auto s2 = send_request_with_tag(67890, terminal2, status2); + + FakeHttpConnectionPtr fake_upstream_connection2; + FakeStreamPtr fake_stream2; + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*BaseIntegrationTest::dispatcher_, + fake_upstream_connection2)); + ASSERT_TRUE( + fake_upstream_connection2->waitForNewStream(*BaseIntegrationTest::dispatcher_, fake_stream2)); + ASSERT_TRUE(fake_stream2->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + fake_stream2->encodeHeaders(Http::TestResponseHeaderMapImpl({{":status", "200"}}), false); + fake_stream2->encodeData(100, true); + terminal2.waitReady(); + ASSERT_EQ(status2, "200"); + + // 3. Drain only the connections matching the first socket tag 12345 + engine->drainConnectionsBySocketTag(12345); + // Directly verify server-side connection 1 disconnects + ASSERT_TRUE(fake_upstream_connection1->waitForDisconnect()); + // Verify server-side connection 2 remains fully open and connected + EXPECT_TRUE(fake_upstream_connection2->connected()); + + // 4. Third request to the same host with the second socket tag 67890 + ConditionalInitializer terminal3; + std::string status3; + auto s3 = send_request_with_tag(67890, terminal3, status3); + + FakeStreamPtr fake_stream3; + ASSERT_TRUE( + fake_upstream_connection2->waitForNewStream(*BaseIntegrationTest::dispatcher_, fake_stream3)); + ASSERT_TRUE(fake_stream3->waitForEndStream(*BaseIntegrationTest::dispatcher_)); + fake_stream3->encodeHeaders(Http::TestResponseHeaderMapImpl({{":status", "200"}}), false); + fake_stream3->encodeData(100, true); + terminal3.waitReady(); + + ASSERT_EQ(status3, "200"); + if (fake_upstream_connection2 != nullptr) { + ASSERT_TRUE(fake_upstream_connection2->close()); + ASSERT_TRUE(fake_upstream_connection2->waitForDisconnect()); + } +} } // namespace } // namespace Envoy diff --git a/mobile/test/common/integration/engine_with_test_server.h b/mobile/test/common/integration/engine_with_test_server.h index e3e066080fdd3..0b5965378c105 100644 --- a/mobile/test/common/integration/engine_with_test_server.h +++ b/mobile/test/common/integration/engine_with_test_server.h @@ -2,7 +2,7 @@ #include "test/common/integration/test_server.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" namespace Envoy { diff --git a/mobile/test/common/integration/rtds_integration_test.cc b/mobile/test/common/integration/rtds_integration_test.cc index 1c9063618ea89..d3656cc6b28cf 100644 --- a/mobile/test/common/integration/rtds_integration_test.cc +++ b/mobile/test/common/integration/rtds_integration_test.cc @@ -5,11 +5,15 @@ #include "test/common/integration/xds_integration_test.h" #include "test/test_common/environment.h" +#include "extension_registry.h" +#include "source/common/tls/server_context_impl.h" #include "test/test_common/test_runtime.h" #include "test/test_common/utility.h" +#include "library/common/api/external.h" #include "gtest/gtest.h" +using testing::Ge; namespace Envoy { namespace { @@ -34,12 +38,21 @@ class RtdsIntegrationTest : public XdsIntegrationTest { xds_builder.addRuntimeDiscoveryService("some_rtds_resource", /*timeout_in_seconds=*/1) .setSslRootCerts(getUpstreamCert()); builder_.setXds(std::move(xds_builder)); + builder_.setLogLevel(Logger::Logger::info); + builder_.enforceTrustChainVerification(false); XdsIntegrationTest::createEnvoy(); } - void SetUp() override { initialize(); } + void SetUp() override {} + + void TearDown() override { + Api::External::unregisterApi("envoy_proxy_resolver"); + XdsIntegrationTest::TearDown(); + } void runReloadTest() { + initialize(); + stream_ = createNewStream(createDefaultStreamCallbacks()); // Send a request on the data plane. stream_->sendHeaders(std::make_unique(default_request_headers_), @@ -73,7 +86,7 @@ class RtdsIntegrationTest : public XdsIntegrationTest { Config::getTypeUrl(), {some_rtds_resource}, {some_rtds_resource}, {}, "1"); // Wait until the RTDS updates from the DiscoveryResponse have been applied. - ASSERT_TRUE(waitForCounterGe(load_success_counter, load_success_value + 1)); + ASSERT_TRUE(waitForCounter(load_success_counter, Ge(load_success_value + 1))); // Verify that the Runtime config values are from the RTDS response. EXPECT_TRUE(Runtime::runtimeFeatureEnabled("envoy.reloadable_features.test_feature_false")); @@ -89,7 +102,7 @@ class RtdsIntegrationTest : public XdsIntegrationTest { Config::getTypeUrl(), {some_rtds_resource}, {some_rtds_resource}, {}, "2", {{"test", Protobuf::Any()}}); // Wait until the RTDS updates from the DiscoveryResponse have been applied. - ASSERT_TRUE(waitForCounterGe(load_success_counter, load_success_value + 1)); + ASSERT_TRUE(waitForCounter(load_success_counter, Ge(load_success_value + 1))); // Verify that the Runtime config values are from the RTDS response. EXPECT_FALSE(Runtime::runtimeFeatureEnabled("envoy.reloadable_features.test_feature_false")); @@ -113,5 +126,10 @@ TEST_P(RtdsIntegrationTest, RtdsReloadWithoutDfpMixedScheme) { runReloadTest(); } +TEST_P(RtdsIntegrationTest, RtdsReloadWithWorkerThread) { + builder_.enableWorkerThread(true); + runReloadTest(); +} + } // namespace } // namespace Envoy diff --git a/mobile/test/common/integration/sds_integration_test.cc b/mobile/test/common/integration/sds_integration_test.cc index 1fa6151ca63a4..2adc422428f38 100644 --- a/mobile/test/common/integration/sds_integration_test.cc +++ b/mobile/test/common/integration/sds_integration_test.cc @@ -9,6 +9,7 @@ #include "gtest/gtest.h" +using testing::Ge; namespace Envoy { namespace { @@ -46,7 +47,7 @@ class SdsIntegrationTest : public XdsIntegrationTest { setUpSdsConfig(secret_config, SECRET_NAME); auto* transport_socket = cds_cluster.mutable_transport_socket(); transport_socket->set_name("envoy.transport_sockets.tls"); - transport_socket->mutable_typed_config()->PackFrom(tls_context); + std::ignore = transport_socket->mutable_typed_config()->PackFrom(tls_context); sendDiscoveryResponse( Config::getTypeUrl(), {cds_cluster}, {cds_cluster}, {}, "55"); @@ -57,7 +58,7 @@ class SdsIntegrationTest : public XdsIntegrationTest { discovery_response.set_version_info("1"); discovery_response.set_type_url( Config::getTypeUrl()); - discovery_response.add_resources()->PackFrom(secret); + std::ignore = discovery_response.add_resources()->PackFrom(secret); xds_stream_->sendGrpcMessage(discovery_response); } @@ -94,17 +95,17 @@ INSTANTIATE_TEST_SUITE_P( TEST_P(SdsIntegrationTest, SdsForUpstreamCluster) { // Wait until the new cluster from CDS is added before sending the SDS response. sendCdsResponse(); - ASSERT_TRUE(waitForCounterGe("cluster_manager.cluster_added", 1)); + ASSERT_TRUE(waitForCounter("cluster_manager.cluster_added", Ge(1))); // Wait until the Envoy instance has obtained an updated secret from the SDS cluster. This // verifies that the SDS API is working from the Envoy client and allows us to know we can start // sending HTTP requests to the upstream cluster using the secret. sendSdsResponse(getClientSecret()); - ASSERT_TRUE(waitForCounterGe(fmt::format("sds.{}.update_success", SECRET_NAME), 1)); + ASSERT_TRUE(waitForCounter(fmt::format("sds.{}.update_success", SECRET_NAME), Ge(1))); ASSERT_TRUE( - waitForCounterGe(fmt::format("cluster.{}.client_ssl_socket_factory.ssl_context_update_by_sds", - XDS_CLUSTER_NAME), - 1)); + waitForCounter(fmt::format("cluster.{}.client_ssl_socket_factory.ssl_context_update_by_sds", + XDS_CLUSTER_NAME), + Ge(1))); } } // namespace diff --git a/mobile/test/common/integration/test_server.cc b/mobile/test/common/integration/test_server.cc index e892c7857a9db..6ba8b99b462b7 100644 --- a/mobile/test/common/integration/test_server.cc +++ b/mobile/test/common/integration/test_server.cc @@ -88,22 +88,23 @@ baseProxyConfig(Network::Address::IpVersion version, bool http, int port) { resolver_config; dns_cache_config->mutable_typed_dns_resolver_config()->set_name( "envoy.network.dns_resolver.getaddrinfo"); - dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( - resolver_config); + std::ignore = + dns_cache_config->mutable_typed_dns_resolver_config()->mutable_typed_config()->PackFrom( + resolver_config); auto* dfp_filter = hcm.add_http_filters(); dfp_filter->set_name("envoy.filters.http.dynamic_forward_proxy"); - dfp_filter->mutable_typed_config()->PackFrom(dfp_config); + std::ignore = dfp_filter->mutable_typed_config()->PackFrom(dfp_config); auto* router_filter = hcm.add_http_filters(); envoy::extensions::filters::http::router::v3::Router router_config; router_filter->set_name("envoy.router"); - router_filter->mutable_typed_config()->PackFrom(router_config); + std::ignore = router_filter->mutable_typed_config()->PackFrom(router_config); envoy::config::listener::v3::FilterChain* filter_chain = listener->add_filter_chains(); auto* filter = filter_chain->add_filters(); filter->set_name("envoy.filters.network.http_connection_manager"); - filter->mutable_typed_config()->PackFrom(hcm); + std::ignore = filter->mutable_typed_config()->PackFrom(hcm); // Base cluster config (DFP cluster config) auto* base_cluster = static_resources->add_clusters(); @@ -114,7 +115,7 @@ baseProxyConfig(Network::Address::IpVersion version, bool http, int port) { envoy::config::cluster::v3::Cluster::CustomClusterType base_cluster_type; base_cluster_config.mutable_dns_cache_config()->CopyFrom(*dns_cache_config); base_cluster_type.set_name("envoy.clusters.dynamic_forward_proxy"); - base_cluster_type.mutable_typed_config()->PackFrom(base_cluster_config); + std::ignore = base_cluster_type.mutable_typed_config()->PackFrom(base_cluster_config); base_cluster->mutable_cluster_type()->CopyFrom(base_cluster_type); return bootstrap; @@ -145,10 +146,6 @@ TestServer::TestServer() void TestServer::start(TestServerType type, int port) { port_ = port; ASSERT(!upstream_); - // pre-setup: see https://github.com/envoyproxy/envoy/blob/main/test/test_runner.cc - Logger::Context logging_state(spdlog::level::level_enum::err, - "[%Y-%m-%d %T.%e][%t][%l][%n] [%g:%#] %v", lock_, false, false); - // end pre-setup Network::DownstreamTransportSocketFactoryPtr factory; Extensions::TransportSockets::Tls::forceRegisterServerContextFactoryImpl(); @@ -189,7 +186,7 @@ void TestServer::start(TestServerType type, int port) { registerMobileProtoDescriptors(); #endif test_server_ = IntegrationTestServer::create( - "", version_, nullptr, nullptr, {}, time_system_, *api_, false, absl::nullopt, + "", version_, nullptr, nullptr, {}, time_system_, *api_, false, std::nullopt, Server::FieldValidationConfig(), 1, std::chrono::seconds(1), Server::DrainStrategy::Gradual, nullptr, false, false, baseProxyConfig(version_, true, port_), false); test_server_->waitUntilListenersReady(); @@ -204,7 +201,7 @@ void TestServer::start(TestServerType type, int port) { registerMobileProtoDescriptors(); #endif test_server_ = IntegrationTestServer::create( - "", version_, nullptr, nullptr, {}, time_system_, *api_, false, absl::nullopt, + "", version_, nullptr, nullptr, {}, time_system_, *api_, false, std::nullopt, Server::FieldValidationConfig(), 1, std::chrono::seconds(1), Server::DrainStrategy::Gradual, nullptr, false, false, baseProxyConfig(version_, false, port_), false); test_server_->waitUntilListenersReady(); diff --git a/mobile/test/common/integration/xds_integration_test.cc b/mobile/test/common/integration/xds_integration_test.cc index 6059366367417..75004abad663a 100644 --- a/mobile/test/common/integration/xds_integration_test.cc +++ b/mobile/test/common/integration/xds_integration_test.cc @@ -88,9 +88,9 @@ XdsIntegrationTest::createSingleEndpointClusterConfig(const std::string& cluster // Set the protocol options. envoy::extensions::upstreams::http::v3::HttpProtocolOptions options; options.mutable_explicit_http_config()->mutable_http2_protocol_options(); - (*config.mutable_typed_extension_protocol_options()) - ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] - .PackFrom(options); + std::ignore = (*config.mutable_typed_extension_protocol_options()) + ["envoy.extensions.upstreams.http.v3.HttpProtocolOptions"] + .PackFrom(options); return config; } diff --git a/mobile/test/common/integration/xds_test_server.cc b/mobile/test/common/integration/xds_test_server.cc index 5ab34b0a3765d..de576e42eff9a 100644 --- a/mobile/test/common/integration/xds_test_server.cc +++ b/mobile/test/common/integration/xds_test_server.cc @@ -42,16 +42,15 @@ XdsTestServer::XdsTestServer() api_->allocateDispatcher("test_thread", Buffer::WatermarkFactoryPtr{mock_buffer_factory_}); ON_CALL(*mock_buffer_factory_, createBuffer_(_, _, _)) - .WillByDefault(Invoke([](std::function below_low, std::function above_high, - std::function above_overflow) -> Buffer::Instance* { - return new Buffer::WatermarkBuffer(std::move(below_low), std::move(above_high), - std::move(above_overflow)); - })); + .WillByDefault( + Invoke([](absl::AnyInvocable below_low, absl::AnyInvocable above_high, + absl::AnyInvocable above_overflow) -> Buffer::Instance* { + return new Buffer::WatermarkBuffer(std::move(below_low), std::move(above_high), + std::move(above_overflow)); + })); ON_CALL(factory_context_.server_context_, api()).WillByDefault(testing::ReturnRef(*api_)); ON_CALL(factory_context_, statsScope()) .WillByDefault(testing::ReturnRef(*stats_store_.rootScope())); - Logger::Context logging_state(spdlog::level::level_enum::err, - "[%Y-%m-%d %T.%e][%t][%l][%n] [%g:%#] %v", lock_, false, false); upstream_config_.upstream_protocol_ = Http::CodecType::HTTP2; Extensions::TransportSockets::Tls::forceRegisterServerContextFactoryImpl(); Config::forceRegisterAdsConfigSubscriptionFactory(); diff --git a/mobile/test/common/internal_engine_test.cc b/mobile/test/common/internal_engine_test.cc index c796bdbca86b6..ec7ceccd369f8 100644 --- a/mobile/test/common/internal_engine_test.cc +++ b/mobile/test/common/internal_engine_test.cc @@ -1,4 +1,5 @@ #include +#include #include @@ -13,8 +14,9 @@ #include "test/test_common/utility.h" #include "absl/synchronization/notification.h" +#include #include "gtest/gtest.h" -#include "library/cc/engine_builder.h" +#include "test/cc/engine_builder_test_shim.h" #include "library/common/api/external.h" #include "library/common/bridge//utility.h" #include "library/common/http/header_utility.h" @@ -155,7 +157,7 @@ TEST_F(InternalEngineTest, AccessEngineAfterInitialization) { absl::Notification getClusterManagerInvoked; // Running engine functions should work because the engine is running - EXPECT_THAT(engine_->engine_->dumpStats(), testing::HasSubstr("runtime.load_success: 1\n")); + EXPECT_THAT(engine_->engine_->dumpStats(), testing::HasSubstr("runtime.load_success: 2\n")); engine_->terminate(); ASSERT_TRUE(engine_->isTerminated()); @@ -342,7 +344,7 @@ TEST_F(InternalEngineTest, BasicStream) { Protobuf::Any typed_config; typed_config.set_type_url("type.googleapis.com/envoy.extensions.filters.http.buffer.v3.Buffer"); std::string serialized_buffer; - buffer.SerializeToString(&serialized_buffer); + std::ignore = buffer.SerializeToString(&serialized_buffer); typed_config.set_value(serialized_buffer); Platform::EngineBuilder builder; @@ -445,9 +447,9 @@ TEST_F(InternalEngineTest, ThreadCreationFailed) { EngineTestContext test_context{}; auto thread_factory = std::make_unique(); EXPECT_CALL(*thread_factory, createThread(_, _, false)).WillOnce(Return(ByMove(nullptr))); - std::unique_ptr engine( - new InternalEngine(createDefaultEngineCallbacks(test_context), {}, {}, {}, {}, false, - std::move(thread_factory))); + std::unique_ptr engine(new InternalEngine( + createDefaultEngineCallbacks(test_context), {}, {}, {}, {}, std::move(thread_factory))); + engine->disableDnsRefreshOnNetworkChange(false); Platform::EngineBuilder builder; envoy_status_t status = runEngine(engine, builder, LOG_LEVEL); EXPECT_EQ(status, ENVOY_FAILURE); @@ -519,4 +521,89 @@ TEST_F(ThreadPriorityInternalEngineTest, SetOutOfRangeThreadPriority) { EXPECT_NE(actual_thread_priority, expected_thread_priority); } +TEST_F(InternalEngineTest, MultipleListenersStream) { + TestServer test_server; + test_server.start(TestServerType::HTTP1_WITHOUT_TLS); + + EngineTestContext test_context{}; + std::unique_ptr engine = std::make_unique( + createDefaultEngineCallbacks(test_context), /*logger=*/nullptr, + /*event_tracker=*/nullptr, /*thread_priority=*/std::nullopt, + /*high_watermark=*/std::nullopt, /*enable_logger=*/true, + /*use_worker_thread=*/true); + + Platform::EngineBuilder builder; + builder.enableWorkerThread(true); + auto bootstrap = builder.generateBootstrap(); + + // Add a second listener by cloning the first one and renaming it. + ASSERT_GT(bootstrap->static_resources().listeners_size(), 0); + auto* listener1 = bootstrap->mutable_static_resources()->mutable_listeners(0); + // Ensure it has the expected name. + ASSERT_EQ(listener1->name(), "base_api_listener"); + + auto* listener2 = bootstrap->mutable_static_resources()->add_listeners(); + listener2->CopyFrom(*listener1); + listener2->set_name("second_api_listener"); + + // Run engine with mutated bootstrap. + auto options = std::make_shared(); + options->setConfigProto(std::move(bootstrap)); + options->setLogLevel(static_cast(LOG_LEVEL)); + engine->run(std::move(options)); + + ASSERT_TRUE(test_context.on_engine_running.WaitForNotificationWithTimeout(absl::Seconds(10))); + + // Start stream on first listener. + { + absl::Notification on_complete_notification; + EnvoyStreamCallbacks stream_callbacks; + stream_callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, + bool /* end_stream */, envoy_stream_intel) { + EXPECT_EQ(headers.Status()->value().getStringView(), "200"); + }; + stream_callbacks.on_complete_ = [&](envoy_stream_intel, envoy_final_stream_intel) { + on_complete_notification.Notify(); + }; + + envoy_stream_t stream = engine->initStream(); + // Explicitly specify "base_api_listener" + engine->startStream(stream, std::move(stream_callbacks), false, "base_api_listener"); + + engine->sendHeaders(stream, createLocalhostRequestHeaders(test_server.getAddress()), false); + engine->sendData(stream, std::make_unique("request body"), false); + engine->sendTrailers(stream, Http::Utility::createRequestTrailerMapPtr()); + + ASSERT_TRUE(on_complete_notification.WaitForNotificationWithTimeout(absl::Seconds(10))); + } + + // Start stream on second listener. + { + absl::Notification on_complete_notification; + EnvoyStreamCallbacks stream_callbacks; + stream_callbacks.on_headers_ = [&](const Http::ResponseHeaderMap& headers, + bool /* end_stream */, envoy_stream_intel) { + EXPECT_EQ(headers.Status()->value().getStringView(), "200"); + }; + stream_callbacks.on_complete_ = [&](envoy_stream_intel, envoy_final_stream_intel) { + on_complete_notification.Notify(); + }; + + envoy_stream_t stream = engine->initStream(); + // Explicitly specify "second_api_listener" + engine->startStream(stream, std::move(stream_callbacks), false, "second_api_listener"); + + engine->sendHeaders(stream, createLocalhostRequestHeaders(test_server.getAddress()), false); + engine->sendData(stream, std::make_unique("request body"), false); + engine->sendTrailers(stream, Http::Utility::createRequestTrailerMapPtr()); + + ASSERT_TRUE(on_complete_notification.WaitForNotificationWithTimeout(absl::Seconds(10))); + } + + test_server.shutdown(); + engine->terminate(); + + ASSERT_TRUE(test_context.on_exit.WaitForNotificationWithTimeout(absl::Seconds(10))); +} + } // namespace Envoy diff --git a/mobile/test/common/stream_info/BUILD b/mobile/test/common/stream_info/BUILD index 528486c353e51..f7d85c7230bb8 100644 --- a/mobile/test/common/stream_info/BUILD +++ b/mobile/test/common/stream_info/BUILD @@ -11,6 +11,5 @@ envoy_cc_test( deps = [ "//library/common/stream_info:extra_stream_info_lib", "@envoy//source/common/stream_info:stream_info_lib", - "@envoy//test/test_common:simulated_time_system_lib", ], ) diff --git a/mobile/test/common/stream_info/extra_stream_info_test.cc b/mobile/test/common/stream_info/extra_stream_info_test.cc index 86031e3229205..535c55bc4a7fe 100644 --- a/mobile/test/common/stream_info/extra_stream_info_test.cc +++ b/mobile/test/common/stream_info/extra_stream_info_test.cc @@ -1,7 +1,5 @@ #include "source/common/stream_info/stream_info_impl.h" -#include "test/test_common/simulated_time_system.h" - #include "gmock/gmock.h" #include "gtest/gtest.h" #include "library/common/stream_info/extra_stream_info.h" diff --git a/mobile/test/java/integration/AndroidEngineSocketTagTest.java b/mobile/test/java/integration/AndroidEngineSocketTagTest.java index c61b7442e7fbd..05fb81b3fb560 100644 --- a/mobile/test/java/integration/AndroidEngineSocketTagTest.java +++ b/mobile/test/java/integration/AndroidEngineSocketTagTest.java @@ -84,6 +84,8 @@ public MockResponse dispatch(RecordedRequest recordedRequest) { assertThat(response.getHeaders().getHttpStatus()).isEqualTo(200); assertThat(response.getBodyAsString()).isEqualTo("This is my response Body"); assertThat(response.getEnvoyError()).isNull(); + + engine.drainConnectionsBySocketTag(2); } private Response sendRequest(RequestScenario requestScenario) throws Exception { diff --git a/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt b/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt index c98996fccae59..6c36f2bbe5878 100644 --- a/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt +++ b/mobile/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt @@ -82,6 +82,7 @@ class EnvoyConfigurationTest { dnsNumRetries: Int? = 3, enableDrainPostDnsRefresh: Boolean = false, enableHttp3: Boolean = true, + enableEarlyData: Boolean = true, http3ConnectionOptions: String = "5RTO", http3ClientConnectionOptions: String = "MPQC", quicHints: Map = mapOf("www.abc.com" to 443, "www.def.com" to 443), @@ -136,6 +137,7 @@ class EnvoyConfigurationTest { dnsNumRetries ?: -1, enableDrainPostDnsRefresh, enableHttp3, + enableEarlyData, http3ConnectionOptions, http3ClientConnectionOptions, quicHints, @@ -327,4 +329,15 @@ class EnvoyConfigurationTest { assertThat(resolvedTemplate).contains("QuicPlatformPacketWriterConfig") assertThat(resolvedTemplate).contains("connection_migration { migrate_idle_connections { } }") } + + @Test + fun `configuration resolves early data policy when early data disabled`() { + JniLibrary.loadTestLibrary() + val envoyConfiguration = buildTestEnvoyConfiguration( + enableEarlyData = false + ) + + val resolvedTemplate = TestJni.createProtoString(envoyConfiguration) + assertThat(resolvedTemplate).contains("envoy.route.early_data_policy.default") + } } diff --git a/mobile/test/java/io/envoyproxy/envoymobile/engine/testing/QuicTestServerTest.java b/mobile/test/java/io/envoyproxy/envoymobile/engine/testing/QuicTestServerTest.java index 67d92c2740895..a97379e0eb603 100644 --- a/mobile/test/java/io/envoyproxy/envoymobile/engine/testing/QuicTestServerTest.java +++ b/mobile/test/java/io/envoyproxy/envoymobile/engine/testing/QuicTestServerTest.java @@ -47,7 +47,7 @@ public void setUpEngine() throws Exception { CountDownLatch latch = new CountDownLatch(1); engine = new AndroidEngineBuilder(appContext) - .setLogLevel(LogLevel.TRACE) + .setLogLevel(LogLevel.INFO) .setLogger((level, message) -> { System.out.print(message); return null; diff --git a/mobile/test/java/org/chromium/net/CronetHttp3Test.java b/mobile/test/java/org/chromium/net/CronetHttp3Test.java index 72d0ac6191b7d..a1fb9a42d2f14 100644 --- a/mobile/test/java/org/chromium/net/CronetHttp3Test.java +++ b/mobile/test/java/org/chromium/net/CronetHttp3Test.java @@ -120,7 +120,7 @@ public void log(int logLevel, String message) { if (setUpLogging) { nativeCronetEngineBuilder.setLogger(logger); - nativeCronetEngineBuilder.setLogLevel(EnvoyEngine.LogLevel.TRACE); + nativeCronetEngineBuilder.setLogLevel(EnvoyEngine.LogLevel.INFO); } if (useAndroidNetworkMonitorV2) { nativeCronetEngineBuilder.setUseV2NetworkMonitor(useAndroidNetworkMonitorV2); diff --git a/mobile/test/java/org/chromium/net/impl/CronvoyEngineTest.java b/mobile/test/java/org/chromium/net/impl/CronvoyEngineTest.java index 2877c10593686..019bd72ebff98 100644 --- a/mobile/test/java/org/chromium/net/impl/CronvoyEngineTest.java +++ b/mobile/test/java/org/chromium/net/impl/CronvoyEngineTest.java @@ -104,10 +104,10 @@ public void get_simple() throws Exception { // Do some basic stats accounting. String stats = cronvoyEngine.getEnvoyEngine().dumpStats(); Map statsMap = StatsUtils.statsToList(stats); - assertThat(statsMap.containsKey("http.hcm.downstream_rq_2xx")); - assertThat(statsMap.containsKey("http.hcm.downstream_total")); - assertThat(statsMap.containsKey("runtime.load_success")); - assertThat(statsMap.get("runtime.load_success")).isEqualTo("1"); + assertThat(statsMap).containsKey("http.hcm.downstream_rq_2xx"); + assertThat(statsMap).containsKey("http.hcm.downstream_rq_total"); + assertThat(statsMap).containsKey("runtime.load_success"); + assertThat(statsMap.get("runtime.load_success")).isEqualTo("2"); } @Test diff --git a/mobile/test/jni/BUILD b/mobile/test/jni/BUILD index 35d8af47050fa..67973dca522dd 100644 --- a/mobile/test/jni/BUILD +++ b/mobile/test/jni/BUILD @@ -160,6 +160,7 @@ cc_library( ], deps = [ "//library/jni:jni_helper_lib", + "@envoy//test/test_common:test_version_linkstamp", ], alwayslink = True, ) @@ -182,6 +183,7 @@ cc_library( deps = [ "//library/common/http:header_utility_lib", "//library/jni:jni_utility_lib", + "@envoy//test/test_common:test_version_linkstamp", "@envoy//test/test_common:utility_lib", ], alwayslink = True, diff --git a/mobile/test/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt b/mobile/test/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt index ef0e190e28000..322d21706974a 100644 --- a/mobile/test/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt +++ b/mobile/test/kotlin/io/envoyproxy/envoymobile/mocks/MockEnvoyEngine.kt @@ -38,6 +38,8 @@ class MockEnvoyEngine : EnvoyEngine { override fun dumpStats(): String = "" + override fun drainConnectionsBySocketTag(tag: Int) = Unit + override fun getEngineHandle(): Long = 0 override fun resetConnectivityState() = Unit diff --git a/mobile/test/objective-c/BUILD b/mobile/test/objective-c/BUILD index 2460ad8698cd9..3aa291e36cad6 100644 --- a/mobile/test/objective-c/BUILD +++ b/mobile/test/objective-c/BUILD @@ -14,6 +14,7 @@ envoy_mobile_objc_test( visibility = ["//visibility:public"], deps = [ "//library/objective-c:envoy_objc_bridge_lib", + "@envoy//test/test_common:test_version_linkstamp", ], ) @@ -27,6 +28,7 @@ envoy_mobile_objc_test( deps = [ "//library/objective-c:envoy_key_value_store_bridge_impl_lib", "//library/objective-c:envoy_objc_bridge_lib", + "@envoy//test/test_common:test_version_linkstamp", ], ) diff --git a/mobile/test/performance/files_em_does_not_use b/mobile/test/performance/files_em_does_not_use index 25abe069b1b0b..01012c5e96da7 100644 --- a/mobile/test/performance/files_em_does_not_use +++ b/mobile/test/performance/files_em_does_not_use @@ -18,7 +18,6 @@ source/extensions/load_balancing_policies/subset/subset_lb.h source/extensions/transport_sockets/tls/downstream_config.h source/common/tls/server_ssl_socket.h source/common/router/rds_impl.h -source/common/router/vhds.h source/common/tls/server_context_config_impl.h source/common/tls/server_context_impl.h source/common/tls/ocsp/ocsp.h diff --git a/mobile/test/python/BUILD b/mobile/test/python/BUILD index fbd9f6a515098..1c533bc1c1abb 100644 --- a/mobile/test/python/BUILD +++ b/mobile/test/python/BUILD @@ -1,7 +1,11 @@ +load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") +load("@mobile_pip3//:requirements.bzl", "requirement") load("@rules_python//python:defs.bzl", "py_library", "py_test") licenses(["notice"]) # Apache 2 +envoy_mobile_package() + py_library( name = "echo_test_server", srcs = ["echo_test_server.py"], @@ -26,3 +30,34 @@ py_test( "//library/python:envoy_mobile_lib", ], ) + +py_test( + name = "httpx_transport_test", + srcs = ["test_httpx_transport.py"], + main = "test_httpx_transport.py", + deps = [ + "//library/python:envoy_mobile_lib", + requirement("httpx"), + ], +) + +py_test( + name = "transport_factory_test", + srcs = ["test_transport_factory.py"], + main = "test_transport_factory.py", + deps = [ + "//library/python:envoy_mobile_lib", + requirement("httpx"), + ], +) + +py_test( + name = "httpx_transport_fetch_test", + srcs = ["httpx_transport_fetch_test.py"], + main = "httpx_transport_fetch_test.py", + deps = [ + ":echo_test_server", + "//library/python:envoy_mobile_lib", + requirement("httpx"), + ], +) diff --git a/mobile/test/python/async_client/BUILD b/mobile/test/python/async_client/BUILD index ad41ddbea5f01..0e8790dcd0225 100644 --- a/mobile/test/python/async_client/BUILD +++ b/mobile/test/python/async_client/BUILD @@ -1,7 +1,10 @@ +load("@envoy//bazel:envoy_build_system.bzl", "envoy_mobile_package") load("@rules_python//python:defs.bzl", "py_test") licenses(["notice"]) # Apache 2 +envoy_mobile_package() + py_test( name = "async_client_fetch_test", srcs = ["async_client_fetch_test.py"], diff --git a/mobile/test/python/async_client/async_client_fetch_test.py b/mobile/test/python/async_client/async_client_fetch_test.py index 84be7740acf35..3a62a6979d339 100644 --- a/mobile/test/python/async_client/async_client_fetch_test.py +++ b/mobile/test/python/async_client/async_client_fetch_test.py @@ -2,7 +2,6 @@ import asyncio import json -import random import unittest from test.python.echo_test_server import EchoTestServer @@ -16,10 +15,9 @@ class TestAsyncClientFetch(unittest.TestCase): @classmethod def setUpClass(cls): """Set up an echo test server for the tests to hit.""" - port = random.randint(2**14, 2**16) - cls._echo_server = EchoTestServer("127.0.0.1", port) + cls._echo_server = EchoTestServer() cls._echo_server.start() - cls._echo_server_url = f"http://127.0.0.1:{port}" + cls._echo_server_url = f"http://{cls._echo_server.url}" @classmethod def tearDownClass(cls): @@ -28,7 +26,11 @@ def tearDownClass(cls): def _make_client_builder(self): # factory to keep constructor logic consistent without sharing instances - builder = EngineBuilder().set_log_level(LogLevel.trace) + builder = ( + EngineBuilder() + .set_log_level(LogLevel.info) + .add_runtime_guard("getaddrinfo_no_ai_flags", True) + ) return builder def test_simple_get_request(self): @@ -107,7 +109,10 @@ async def run(): ), client.get(f"{self._echo_server_url}/"), ] - responses = await asyncio.gather(*tasks) + responses = await asyncio.wait_for( + asyncio.gather(*tasks), + timeout=30, + ) for resp in responses: async with resp: @@ -225,6 +230,21 @@ async def run(): asyncio.run(run()) + def test_non_existent_listener(self): + """Verify that using a non-existent listener name fails.""" + + async def run(): + async with AsyncClient( + self._make_client_builder(), listener_name="non_existent_listener" + ) as client: + with self.assertRaises(ClientResponseError) as cm: + await client.get(f"{self._echo_server_url}/") + self.assertIsNotNone(cm.exception.envoy_error) + self.assertIn("Listener not found", cm.exception.envoy_error.message) + self.assertIn("non_existent_listener", cm.exception.envoy_error.message) + + asyncio.run(run()) + if __name__ == "__main__": unittest.main() diff --git a/mobile/test/python/echo_test_server.py b/mobile/test/python/echo_test_server.py index 6199b9a8df671..dbb6107408ab8 100644 --- a/mobile/test/python/echo_test_server.py +++ b/mobile/test/python/echo_test_server.py @@ -3,6 +3,8 @@ from threading import Event from threading import Thread import json +import random +import socket class EchoServerHandler(BaseHTTPRequestHandler): @@ -67,15 +69,51 @@ def log_message(self, *args, **kwargs): pass +class HTTPServerV6(HTTPServer): + address_family = socket.AF_INET6 + + +def is_ipv4_supported(): + try: + socket.getaddrinfo("127.0.0.1", None, family=socket.AF_INET) + return True + except socket.gaierror: + return False + + +def is_ipv6_supported(): + try: + socket.getaddrinfo("::1", None, family=socket.AF_INET6) + return True + except socket.gaierror: + return False + + class EchoTestServer: - def __init__(self, ip, port): - self._server = HTTPServer((ip, port), EchoServerHandler) + def __init__(self): + use_v4 = is_ipv4_supported() + if not (use_v4 or is_ipv6_supported()): + raise RuntimeError("Neither IPv4 nor IPv6 is supported by the environment") + + port = random.randint(2**14, 2**16) + server = None + if use_v4: + server = HTTPServer(("127.0.0.1", port), EchoServerHandler) + self.url = f"127.0.0.1:{port}" + else: + server = HTTPServerV6(("::1", port), EchoServerHandler) + self.url = f"[::1]:{port}" + + self._server = server + assert self._server is not None self._server_thread = Thread(target=self._server.serve_forever, daemon=True) + self._server.socket.settimeout(10.0) def start(self): self._server_thread.start() def stop(self): + assert self._server is not None self._server.shutdown() self._server_thread.join() diff --git a/mobile/test/python/fetch_test.py b/mobile/test/python/fetch_test.py index 0a6209c692448..99b49974c41ef 100644 --- a/mobile/test/python/fetch_test.py +++ b/mobile/test/python/fetch_test.py @@ -1,6 +1,5 @@ """Integration tests for the Envoy Mobile Python bindings.""" -import random import threading import unittest from test.python.echo_test_server import EchoTestServer @@ -21,10 +20,9 @@ class TestFetchRequest(unittest.TestCase): @classmethod def setUpClass(cls): """Set up an echo test server for the tests to hit.""" - port = random.randint(2**14, 2**16) - cls._echo_server = EchoTestServer("127.0.0.1", port) + cls._echo_server = EchoTestServer() cls._echo_server.start() - cls._echo_server_url = f"127.0.0.1:{port}" + cls._echo_server_url = cls._echo_server.url @classmethod def tearDownClass(cls): @@ -36,9 +34,11 @@ def _build_engine(self): engine_running = threading.Event() engine = ( EngineBuilder() - .set_log_level(LogLevel.trace) + .set_log_level(LogLevel.info) .add_runtime_guard("dns_cache_set_ip_version_to_remove", True) + .add_runtime_guard("getaddrinfo_no_ai_flags", True) .set_on_engine_running(lambda: engine_running.set()) + .enable_worker_thread(True) .build() ) self.assertTrue(engine_running.wait(timeout=30), "Engine did not start within timeout") diff --git a/mobile/test/python/httpx_transport_fetch_test.py b/mobile/test/python/httpx_transport_fetch_test.py new file mode 100644 index 0000000000000..69c1895c9e32e --- /dev/null +++ b/mobile/test/python/httpx_transport_fetch_test.py @@ -0,0 +1,92 @@ +"""Integration tests for connection pool draining in Envoy Mobile Python transports.""" + +import asyncio +import threading +import unittest +from unittest import mock +from test.python.echo_test_server import EchoTestServer + +from envoy_mobile import ( + EngineBuilder, + EnvoyClientTransport, + AsyncEnvoyClientTransport, + LogLevel, +) +import httpx + + +class HttpxTransportFetchTest(unittest.TestCase): + """Tests verifying targeted connection pool draining on transport close.""" + + @classmethod + def setUpClass(cls): + """Set up an echo test server for the tests to hit.""" + cls._echo_server = EchoTestServer() + cls._echo_server.start() + cls._echo_server_url = f"http://{cls._echo_server.url}" + + @classmethod + def tearDownClass(cls): + """Shut down the echo test server.""" + cls._echo_server.stop() + + def _build_engine(self): + """Helper to build and start an engine with stats and socket tagging enabled.""" + engine_running = threading.Event() + engine = ( + EngineBuilder() + .set_log_level(LogLevel.info) + .enable_stats_collection(True) + .enable_socket_tagging(True) + .add_runtime_guard("dns_cache_set_ip_version_to_remove", True) + .add_runtime_guard("getaddrinfo_no_ai_flags", True) + .set_on_engine_running(lambda: engine_running.set()) + .enable_worker_thread(True) + .build() + ) + self.assertTrue(engine_running.wait(timeout=30), "Engine did not start within timeout") + return engine + + def test_basic_request_with_close(self): + """Verify that closing a synchronous transport drains its isolated connections.""" + engine = self._build_engine() + transport = EnvoyClientTransport(engine) + mock_engine = mock.MagicMock(wraps=engine) + transport._engine = mock_engine + client = httpx.Client(transport=transport) + + # Make a successful request to establish an upstream connection + response = client.get(self._echo_server_url) + self.assertEqual(response.status_code, 200) + + # Close the transport (this triggers drain_connections_by_socket_tag) + client.close() + mock_engine.drain_connections_by_socket_tag.assert_called_once_with( + transport._transport_tag + ) + + engine.terminate() + + def test_async_basic_request_with_close(self): + """Verify that async closing an async transport drains its isolated connections.""" + engine = self._build_engine() + + async def run_request(): + transport = AsyncEnvoyClientTransport(engine) + mock_engine = mock.MagicMock(wraps=engine) + transport._engine = mock_engine + client = httpx.AsyncClient(transport=transport) + response = await client.get(self._echo_server_url) + self.assertEqual(response.status_code, 200) + + await client.aclose() + mock_engine.drain_connections_by_socket_tag.assert_called_once_with( + transport._transport_tag + ) + + asyncio.run(run_request()) + engine.terminate() + + +if __name__ == "__main__": + unittest.main() diff --git a/mobile/test/python/test_httpx_transport.py b/mobile/test/python/test_httpx_transport.py new file mode 100644 index 0000000000000..39e4ca2ed7a46 --- /dev/null +++ b/mobile/test/python/test_httpx_transport.py @@ -0,0 +1,389 @@ +"""Unit tests for Envoy Client httpx transports. + +These tests use unittest.mock to simulate the Envoy Mobile C++ engine (envoy_engine). +We verify that the transports correctly map httpx requests to Envoy headers, +handle streaming request/response bodies, and propagate errors appropriately. +""" + +import asyncio +import sys +import unittest +from unittest.mock import MagicMock, patch, call + +# Mock envoy_engine before any imports that might use it. +# This is necessary because envoy_engine is a C++ extension module that +# may not be available in all test environments. +mock_envoy_engine = MagicMock() +# The code does `from .. import envoy_engine` from inside envoy_mobile.async_client_transport +# So it looks for `envoy_mobile.envoy_engine` +sys.modules["envoy_mobile.envoy_engine"] = mock_envoy_engine + +import httpx +from envoy_mobile import ( + AsyncEnvoyClientTransport, + EnvoyClientTransport, +) + + +# Define these for use in tests since we mocked the module. +class MockEnvoyError: + def __init__(self): + self.error_code = 0 + self.message = "" + + +mock_envoy_engine.EnvoyError = MockEnvoyError + + +class TestEnvoyClientTransport(unittest.TestCase): + """Tests for the synchronous EnvoyClientTransport.""" + + def setUp(self): + self.mock_engine = MagicMock() + self.mock_stream_client = MagicMock() + self.mock_engine.stream_client.return_value = self.mock_stream_client + self.mock_prototype = MagicMock() + self.mock_stream_client.new_stream_prototype.return_value = self.mock_prototype + self.mock_stream = MagicMock() + self.mock_prototype.start.return_value = self.mock_stream + self.transport = EnvoyClientTransport(self.mock_engine) + + def test_handle_request_success(self): + """Verify a successful synchronous request/response cycle.""" + request = httpx.Request("GET", "https://example.com/path") + + def side_effect(*args, **kwargs): + # Simulate Envoy receiving headers and data. + on_headers = kwargs.get("on_headers") + on_headers({":status": "200", "content-type": ["application/json"]}, False, None) + on_data = kwargs.get("on_data") + on_data(b'{"key": "value"}', 16, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = self.transport.handle_request(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["content-type"], "application/json") + self.assertEqual(response.read(), b'{"key": "value"}') + + # Verify Envoy pseudo-headers were correctly mapped. + # Note: In the simplified logic, headers are always sent with end_stream=False. + self.mock_stream.send_headers.assert_called_once() + sent_headers = self.mock_stream.send_headers.call_args[0][0] + end_stream = self.mock_stream.send_headers.call_args[0][1] + self.assertEqual(sent_headers[":method"], "GET") + self.assertFalse(end_stream) + + # Verify the stream was closed with empty data. + self.mock_stream.close.assert_called_once_with(b"") + + def test_handle_request_error(self): + """Verify that Envoy errors are mapped to httpx exceptions.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_error = kwargs.get("on_error") + error = MockEnvoyError() + error.error_code = 2 # ConnectionFailure + error.message = "Connection failed" + on_error(error, None, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + with self.assertRaises(httpx.ConnectError): + self.transport.handle_request(request) + + def test_handle_request_streaming(self): + """Verify that request bodies are streamed to Envoy chunk-by-chunk.""" + + def stream_generator(): + yield b"chunk1" + yield b"chunk2" + + request = httpx.Request("POST", "https://example.com/stream", content=stream_generator()) + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + on_headers({":status": "200"}, False, None) + on_data = kwargs.get("on_data") + on_data(b"ok", 2, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = self.transport.handle_request(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.read(), b"ok") + + # Verify call sequence: headers, chunk1, chunk2, close. + self.mock_stream.send_headers.assert_called_once_with(unittest.mock.ANY, False) + self.mock_stream.send_data.assert_has_calls( + [ + call(b"chunk1"), + call(b"chunk2"), + ] + ) + self.mock_stream.close.assert_called_once_with(b"") + + def test_handle_request_duplicate_headers(self): + """Verify that duplicate headers are correctly preserved in sync transport.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + on_headers( + { + ":status": "200", + "content-type": "application/json", + "set-cookie": ["cookie1", "cookie2"], + }, + False, + None, + ) + on_data = kwargs.get("on_data") + on_data(b"ok", 2, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = self.transport.handle_request(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["content-type"], "application/json") + self.assertEqual(response.headers.get_list("set-cookie"), ["cookie1", "cookie2"]) + + def test_handle_request_socket_tag(self): + """Verify that the transport tag is injected into the Envoy headers in sync transport.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + on_headers({":status": "200"}, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + # We can inspect the transport's tag before running the request + expected_tag = self.transport._transport_tag + expected_header_value = f"0,{expected_tag}" + + self.transport.handle_request(request) + + # Verify send_headers was called + self.mock_stream.send_headers.assert_called_once() + + # Extract the headers passed to Envoy + sent_headers = self.mock_stream.send_headers.call_args[0][0] + + # Verify the socket tag header is present and has the correct value + self.assertIn("x-envoy-mobile-socket-tag", sent_headers) + self.assertEqual(sent_headers["x-envoy-mobile-socket-tag"], expected_header_value) + + def test_handle_request_filtered_headers(self): + """Verify that HTTP/1.1 connection-specific headers (forbidden in H2) and duplicate host headers are filtered out.""" + request = httpx.Request( + "GET", + "https://example.com/path", + headers={ + "connection": "upgrade", + "keep-alive": "timeout=5", + "host": "another-host.com", + "custom-header": "custom-value", + }, + ) + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + on_headers({":status": "200"}, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + self.transport.handle_request(request) + + # Verify send_headers was called + self.mock_stream.send_headers.assert_called_once() + sent_headers = self.mock_stream.send_headers.call_args[0][0] + + # Verify pseudo-headers are present + self.assertEqual(sent_headers[":authority"], "example.com") + + # Verify custom headers are preserved + self.assertEqual(sent_headers["custom-header"], "custom-value") + + # Verify forbidden headers are filtered out + self.assertNotIn("connection", sent_headers) + self.assertNotIn("keep-alive", sent_headers) + self.assertNotIn("host", sent_headers) + + +class TestAsyncEnvoyClientTransport(unittest.IsolatedAsyncioTestCase): + """Tests for the asynchronous AsyncEnvoyClientTransport.""" + + async def asyncSetUp(self): + self.mock_engine = MagicMock() + self.mock_stream_client = MagicMock() + self.mock_engine.stream_client.return_value = self.mock_stream_client + self.mock_prototype = MagicMock() + self.mock_stream_client.new_stream_prototype.return_value = self.mock_prototype + self.mock_stream = MagicMock() + self.mock_prototype.start.return_value = self.mock_stream + self.transport = AsyncEnvoyClientTransport(self.mock_engine) + + async def test_handle_async_request_success(self): + """Verify a successful asynchronous request/response cycle.""" + request = httpx.Request("POST", "https://example.com/post", content=b"body") + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + # Callbacks must be scheduled on the event loop to simulate the AsyncioExecutor. + loop = asyncio.get_running_loop() + loop.call_soon(on_headers, {":status": "201"}, False, None) + on_data = kwargs.get("on_data") + loop.call_soon(on_data, b"created", 7, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = await self.transport.handle_async_request(request) + + self.assertEqual(response.status_code, 201) + self.assertEqual(await response.aread(), b"created") + + self.mock_stream.send_headers.assert_called_once_with(unittest.mock.ANY, False) + self.mock_stream.send_data.assert_called_once_with(b"body") + self.mock_stream.close.assert_called_with(b"") + + async def test_handle_async_request_error(self): + """Verify that Envoy errors are mapped to httpx exceptions asynchronously.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_error = kwargs.get("on_error") + error = MockEnvoyError() + error.error_code = 4 # RequestTimeout + error.message = "Timed out" + loop = asyncio.get_running_loop() + loop.call_soon(on_error, error, None, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + with self.assertRaises(httpx.ReadTimeout): + await self.transport.handle_async_request(request) + + async def test_handle_async_request_streaming(self): + """Verify asynchronous request body streaming.""" + + async def stream_generator(): + yield b"chunk1" + yield b"chunk2" + + request = httpx.Request("POST", "https://example.com/stream", content=stream_generator()) + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + loop = asyncio.get_running_loop() + loop.call_soon(on_headers, {":status": "200"}, False, None) + on_data = kwargs.get("on_data") + loop.call_soon(on_data, b"ok", 2, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = await self.transport.handle_async_request(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(await response.aread(), b"ok") + + self.mock_stream.send_headers.assert_called_once_with(unittest.mock.ANY, False) + self.mock_stream.send_data.assert_has_calls( + [ + call(b"chunk1"), + call(b"chunk2"), + ] + ) + self.mock_stream.close.assert_called_once_with(b"") + + async def test_handle_async_request_duplicate_headers(self): + """Verify that duplicate headers are correctly preserved and exposed via HTTPX.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + loop = asyncio.get_running_loop() + loop.call_soon( + on_headers, + { + ":status": "200", + "content-type": "application/json", + "set-cookie": ["cookie1", "cookie2"], + }, + False, + None, + ) + on_data = kwargs.get("on_data") + loop.call_soon(on_data, b"ok", 2, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + response = await self.transport.handle_async_request(request) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers["content-type"], "application/json") + self.assertEqual(response.headers.get_list("set-cookie"), ["cookie1", "cookie2"]) + + async def test_handle_async_request_socket_tag(self): + """Verify that the transport tag is injected into the Envoy headers.""" + request = httpx.Request("GET", "https://example.com") + + def side_effect(*args, **kwargs): + on_headers = kwargs.get("on_headers") + loop = asyncio.get_running_loop() + loop.call_soon(on_headers, {":status": "200"}, True, None) + return self.mock_stream + + self.mock_prototype.start.side_effect = side_effect + + # We can inspect the transport's tag before running the request + expected_tag = self.transport._transport_tag + expected_header_value = f"0,{expected_tag}" + + await self.transport.handle_async_request(request) + + # Verify send_headers was called + self.mock_stream.send_headers.assert_called_once() + + # Extract the headers passed to Envoy + sent_headers = self.mock_stream.send_headers.call_args[0][0] + + # Verify the socket tag header is present and has the correct value + self.assertIn("x-envoy-mobile-socket-tag", sent_headers) + self.assertEqual(sent_headers["x-envoy-mobile-socket-tag"], expected_header_value) + + +from envoy_mobile.async_client.executor import AsyncioExecutor + + +class TestAsyncioExecutor(unittest.TestCase): + """Tests for the AsyncioExecutor thread-safety wrapper.""" + + def test_strict_failure_mode(self): + """Ensure that the executor fails if used without an active loop.""" + # Instantiate without a running event loop. + executor = AsyncioExecutor(loop=None) + + def my_func(): + pass + + wrapped = executor.wrap(my_func) + # Invoking the wrapped function should raise RuntimeError to protect thread-safety. + with self.assertRaises(RuntimeError): + wrapped() + + +if __name__ == "__main__": + unittest.main() diff --git a/mobile/test/python/test_transport_factory.py b/mobile/test/python/test_transport_factory.py new file mode 100644 index 0000000000000..5b661c5f56327 --- /dev/null +++ b/mobile/test/python/test_transport_factory.py @@ -0,0 +1,114 @@ +"""Unit tests for EnvoyTransportFactory.""" + +import sys +import unittest +from unittest.mock import MagicMock, patch + +# Mock envoy_engine before any imports that might use it +mock_envoy_engine = MagicMock() +sys.modules["envoy_mobile.envoy_engine"] = mock_envoy_engine + +from envoy_mobile import ( + EnvoyTransportFactory, + AsyncEnvoyClientTransport, + EnvoyClientTransport, +) + + +class TestEnvoyTransportFactory(unittest.TestCase): + def setUp(self): + # Reset the shared engine state before each test + EnvoyTransportFactory._engine = None + mock_envoy_engine.reset_mock() + + def _setup_mock_builder(self, mock_builder, mock_engine, auto_fire_callback=True): + """Helper to configure a mock builder. + + By default, it simulates the real engine by immediately invoking the + registered 'on_engine_running' callback when build() is called. + """ + running_callback = None + + def set_on_engine_running(callback): + nonlocal running_callback + running_callback = callback + return mock_builder + + def build(): + if auto_fire_callback and running_callback: + running_callback() + return mock_engine + + mock_builder.set_on_engine_running.side_effect = set_on_engine_running + mock_builder.build.side_effect = build + + def test_get_shared_engine_singleton(self): + # Setup mocks + mock_engine = MagicMock() + mock_builder = MagicMock() + self._setup_mock_builder(mock_builder, mock_engine) + + # Call factory multiple times + engine1 = EnvoyTransportFactory.get_shared_engine(mock_builder) + engine2 = EnvoyTransportFactory.get_shared_engine(mock_builder) + + # Verify same instance returned + self.assertEqual(engine1, engine2) + self.assertEqual(engine1, mock_engine) + + # Verify build was only called once + mock_builder.build.assert_called_once() + + def test_get_transports_use_shared_engine(self): + # Setup mocks + mock_engine = MagicMock() + mock_builder = MagicMock() + self._setup_mock_builder(mock_builder, mock_engine) + + # Get transports + async_transport = EnvoyTransportFactory.get_async_transport(mock_builder) + sync_transport = EnvoyTransportFactory.get_sync_transport(mock_builder) + + # Verify they use the same engine instance + self.assertEqual(async_transport._engine, mock_engine) + self.assertEqual(sync_transport._engine, mock_engine) + + # Verify build was only called once + mock_builder.build.assert_called_once() + + def test_default_builder_initialization(self): + # Setup mocks + mock_builder_instance = MagicMock() + mock_envoy_engine.EngineBuilder.return_value = mock_builder_instance + mock_engine = MagicMock() + self._setup_mock_builder(mock_builder_instance, mock_engine) + + # Call without explicit builder + engine = EnvoyTransportFactory.get_shared_engine() + + # Verify default builder was used + mock_envoy_engine.EngineBuilder.assert_called_once() + self.assertEqual(engine, mock_engine) + + def test_get_shared_engine_timeout(self): + # Setup mocks + mock_engine = MagicMock() + mock_builder = MagicMock() + # Disable auto-fire of callback + self._setup_mock_builder(mock_builder, mock_engine, auto_fire_callback=False) + + # Patch threading.Event.wait to return False immediately, simulating a timeout + with patch("threading.Event.wait", return_value=False): + with self.assertRaises(RuntimeError) as context: + EnvoyTransportFactory.get_shared_engine(mock_builder) + + self.assertIn( + "Envoy Mobile engine failed to start within 10 seconds", str(context.exception) + ) + + mock_builder.set_on_engine_running.assert_called_once() + mock_builder.build.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/mobile/test/swift/EngineBuilderTests.swift b/mobile/test/swift/EngineBuilderTests.swift index 2c562261b7351..51dc98c2bca0f 100644 --- a/mobile/test/swift/EngineBuilderTests.swift +++ b/mobile/test/swift/EngineBuilderTests.swift @@ -27,13 +27,13 @@ final class EngineBuilderTests: XCTestCase { func testAddingLogLevelAddsLogLevelWhenRunningEnvoy() { let expectation = self.expectation(description: "Run called with expected data") MockEnvoyEngine.onRunWithConfig = { _, logLevel in - XCTAssertEqual("trace", logLevel) + XCTAssertEqual("info", logLevel) expectation.fulfill() } _ = EngineBuilder() .addEngineType(MockEnvoyEngine.self) - .setLogLevel(.trace) + .setLogLevel(.info) .build() self.waitForExpectations(timeout: 0.01) } diff --git a/mobile/tools/python/BUILD b/mobile/tools/python/BUILD index 779d1695d3b7c..82bd9838ab7f9 100644 --- a/mobile/tools/python/BUILD +++ b/mobile/tools/python/BUILD @@ -1 +1,16 @@ +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + licenses(["notice"]) # Apache 2 + +exports_files(glob(["**"])) + +compile_pip_requirements( + name = "requirements", + src = "requirements.in", + extra_args = [ + "--allow-unsafe", + "--generate-hashes", + "--reuse-hashes", + "--resolver=backtracking", + ], +) diff --git a/mobile/tools/python/requirements.in b/mobile/tools/python/requirements.in index 7e66a17d49cb8..2f5c6ffbb0fec 100644 --- a/mobile/tools/python/requirements.in +++ b/mobile/tools/python/requirements.in @@ -1 +1,7 @@ black +httpx +mypy +pytest +pytest-asyncio +pytest-benchmark +requests diff --git a/mobile/tools/python/requirements.txt b/mobile/tools/python/requirements.txt index 80b8f1d236fb4..ce9978ec3c74f 100644 --- a/mobile/tools/python/requirements.txt +++ b/mobile/tools/python/requirements.txt @@ -2,8 +2,12 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --allow-unsafe --generate-hashes requirements.in +# bazel run //tools/python:requirements.update # +anyio==4.13.0 \ + --hash=sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708 \ + --hash=sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc + # via httpx black==26.3.1 \ --hash=sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c \ --hash=sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7 \ @@ -33,26 +37,358 @@ black==26.3.1 \ --hash=sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56 \ --hash=sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568 # via -r requirements.in +certifi==2026.4.22 \ + --hash=sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a \ + --hash=sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580 + # via + # httpcore + # httpx + # requests +charset-normalizer==3.4.7 \ + --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \ + --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \ + --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \ + --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \ + --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \ + --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \ + --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \ + --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \ + --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \ + --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \ + --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \ + --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \ + --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \ + --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \ + --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \ + --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \ + --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \ + --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \ + --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \ + --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \ + --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \ + --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \ + --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \ + --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \ + --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \ + --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \ + --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \ + --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \ + --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \ + --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \ + --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \ + --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \ + --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \ + --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \ + --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \ + --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \ + --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \ + --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \ + --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \ + --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \ + --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \ + --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \ + --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \ + --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \ + --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \ + --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \ + --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \ + --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \ + --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \ + --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \ + --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \ + --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \ + --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \ + --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \ + --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \ + --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \ + --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \ + --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \ + --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \ + --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \ + --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \ + --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \ + --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \ + --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \ + --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \ + --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \ + --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \ + --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \ + --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \ + --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \ + --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \ + --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \ + --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \ + --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \ + --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \ + --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \ + --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \ + --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \ + --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \ + --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \ + --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \ + --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \ + --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \ + --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \ + --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \ + --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \ + --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \ + --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \ + --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \ + --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \ + --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \ + --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \ + --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \ + --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \ + --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \ + --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \ + --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \ + --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \ + --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \ + --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \ + --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \ + --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \ + --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \ + --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \ + --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \ + --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \ + --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \ + --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \ + --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \ + --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \ + --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \ + --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \ + --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \ + --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \ + --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \ + --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \ + --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \ + --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \ + --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \ + --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \ + --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \ + --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \ + --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \ + --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \ + --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \ + --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \ + --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \ + --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \ + --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464 + # via requests click==8.3.1 \ --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 # via black +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 + # via httpcore +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 + # via httpx +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad + # via -r requirements.in +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc + # via + # anyio + # httpx + # requests +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest +librt==0.9.0 \ + --hash=sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d \ + --hash=sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d \ + --hash=sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38 \ + --hash=sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8 \ + --hash=sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a \ + --hash=sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb \ + --hash=sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6 \ + --hash=sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499 \ + --hash=sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2 \ + --hash=sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285 \ + --hash=sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5 \ + --hash=sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0 \ + --hash=sha256:2d03fa4fd277a7974c1978c92c374c57f44edeee163d147b477b143446ad1bf6 \ + --hash=sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443 \ + --hash=sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b \ + --hash=sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745 \ + --hash=sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb \ + --hash=sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228 \ + --hash=sha256:42ff8a962554c350d4a83cf47d9b7b78b0e6ff7943e87df7cdfc97c07f3c016f \ + --hash=sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c \ + --hash=sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845 \ + --hash=sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5 \ + --hash=sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c \ + --hash=sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f \ + --hash=sha256:5112c2fb7c2eefefaeaf5c97fec81343ef44ee86a30dcfaa8223822fba6467b4 \ + --hash=sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54 \ + --hash=sha256:54d412e47c21b85865676ed0724e37a89e9593c2eee1e7367adf85bfad56ffb1 \ + --hash=sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236 \ + --hash=sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f \ + --hash=sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27 \ + --hash=sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b \ + --hash=sha256:657f8ba7b9eaaa82759a104137aed2a3ef7bc46ccfd43e0d89b04005b3e0a4cc \ + --hash=sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858 \ + --hash=sha256:6788207daa0c19955d2b668f3294a368d19f67d9b5f274553fd073c1260cbb9f \ + --hash=sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b \ + --hash=sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938 \ + --hash=sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a \ + --hash=sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b \ + --hash=sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d \ + --hash=sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f \ + --hash=sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71 \ + --hash=sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22 \ + --hash=sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8 \ + --hash=sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990 \ + --hash=sha256:81107843ed1836874b46b310f9b1816abcb89912af627868522461c3b7333c0f \ + --hash=sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2 \ + --hash=sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd \ + --hash=sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076 \ + --hash=sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671 \ + --hash=sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9 \ + --hash=sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15 \ + --hash=sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4 \ + --hash=sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f \ + --hash=sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8 \ + --hash=sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d \ + --hash=sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265 \ + --hash=sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61 \ + --hash=sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519 \ + --hash=sha256:a81eea9b999b985e4bacc650c4312805ea7008fd5e45e1bf221310176a7bcb3a \ + --hash=sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40 \ + --hash=sha256:aa95738a68cedd3a6f5492feddc513e2e166b50602958139e47bbdd82da0f5a7 \ + --hash=sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f \ + --hash=sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e \ + --hash=sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9 \ + --hash=sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a \ + --hash=sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3 \ + --hash=sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee \ + --hash=sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11 \ + --hash=sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4 \ + --hash=sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283 \ + --hash=sha256:d9da80e5b04acce03ced8ba6479a71c2a2edf535c2acc0d09c80d2f80f3bad15 \ + --hash=sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084 \ + --hash=sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e \ + --hash=sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882 \ + --hash=sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f \ + --hash=sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e \ + --hash=sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774 \ + --hash=sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce \ + --hash=sha256:eea1b54943475f51698f85fa230c65ccac769f1e603b981be060ac5763d90927 \ + --hash=sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6 \ + --hash=sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118 \ + --hash=sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4 \ + --hash=sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e \ + --hash=sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1 \ + --hash=sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f \ + --hash=sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2 \ + --hash=sha256:f48c963a76d71b9d7927eb817b543d0dccd52ab6648b99d37bd54f4cd475d856 \ + --hash=sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1 \ + --hash=sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f \ + --hash=sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a + # via mypy +mypy==1.20.2 \ + --hash=sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb \ + --hash=sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98 \ + --hash=sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99 \ + --hash=sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100 \ + --hash=sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744 \ + --hash=sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f \ + --hash=sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609 \ + --hash=sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6 \ + --hash=sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c \ + --hash=sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30 \ + --hash=sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4 \ + --hash=sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b \ + --hash=sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558 \ + --hash=sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2 \ + --hash=sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102 \ + --hash=sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc \ + --hash=sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c \ + --hash=sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2 \ + --hash=sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517 \ + --hash=sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58 \ + --hash=sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997 \ + --hash=sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6 \ + --hash=sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c \ + --hash=sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8 \ + --hash=sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14 \ + --hash=sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec \ + --hash=sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee \ + --hash=sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066 \ + --hash=sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563 \ + --hash=sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330 \ + --hash=sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67 \ + --hash=sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15 \ + --hash=sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac \ + --hash=sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3 \ + --hash=sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254 \ + --hash=sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3 \ + --hash=sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4 \ + --hash=sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9 \ + --hash=sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382 \ + --hash=sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943 \ + --hash=sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924 \ + --hash=sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665 \ + --hash=sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026 \ + --hash=sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd + # via -r requirements.in mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 - # via black + # via + # black + # mypy packaging==26.0 \ --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 - # via black + # via + # black + # pytest pathspec==1.0.4 \ --hash=sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645 \ --hash=sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723 - # via black + # via + # black + # mypy platformdirs==4.9.4 \ --hash=sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934 \ --hash=sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 # via black +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +py-cpuinfo==9.0.0 \ + --hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \ + --hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5 + # via pytest-benchmark +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 + # via pytest +pytest==9.0.3 \ + --hash=sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9 \ + --hash=sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c + # via + # -r requirements.in + # pytest-asyncio + # pytest-benchmark +pytest-asyncio==1.4.0 \ + --hash=sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1 \ + --hash=sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42 + # via -r requirements.in +pytest-benchmark==5.2.3 \ + --hash=sha256:bc839726ad20e99aaa0d11a127445457b4219bdb9e80a1afc4b51da7f96b0803 \ + --hash=sha256:deb7317998a23c650fd4ff76e1230066a76cb45dcece0aca5607143c619e7779 + # via -r requirements.in pytokens==0.4.1 \ --hash=sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1 \ --hash=sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009 \ @@ -97,3 +433,18 @@ pytokens==0.4.1 \ --hash=sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6 \ --hash=sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324 # via black +requests==2.33.1 \ + --hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \ + --hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a + # via -r requirements.in +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # via + # anyio + # mypy + # pytest-asyncio +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests diff --git a/source/common/access_log/BUILD b/source/common/access_log/BUILD index 431b5f9f78861..1b3c8682786ef 100644 --- a/source/common/access_log/BUILD +++ b/source/common/access_log/BUILD @@ -44,6 +44,8 @@ envoy_cc_library( deps = [ "//envoy/access_log:access_log_interface", "//envoy/api:api_interface", + "//envoy/stats:stats_macros", + "//envoy/stats:store_interface", "//source/common/buffer:buffer_lib", "//source/common/common:logger_lib", "//source/common/common:thread_lib", diff --git a/source/common/access_log/access_log_impl.cc b/source/common/access_log/access_log_impl.cc index 0752357de3456..7a51caafe8fe4 100644 --- a/source/common/access_log/access_log_impl.cc +++ b/source/common/access_log/access_log_impl.cc @@ -1,6 +1,7 @@ #include "source/common/access_log/access_log_impl.h" #include +#include #include #include "envoy/common/time.h" @@ -24,8 +25,6 @@ #include "source/common/stream_info/utility.h" #include "source/common/tracing/http_tracer_impl.h" -#include "absl/types/optional.h" - namespace Envoy { namespace AccessLog { @@ -117,7 +116,7 @@ bool StatusCodeFilter::evaluate(const Formatter::Context&, } bool DurationFilter::evaluate(const Formatter::Context&, const StreamInfo::StreamInfo& info) const { - absl::optional duration = info.currentDuration(); + std::optional duration = info.currentDuration(); if (!duration.has_value()) { return false; } @@ -214,8 +213,12 @@ HeaderFilter::HeaderFilter(const envoy::config::accesslog::v3::HeaderFilter& con bool HeaderFilter::evaluate(const Formatter::Context& context, const StreamInfo::StreamInfo&) const { - return header_data_->matchesHeaders( - context.requestHeaders().value_or(*Http::StaticEmptyHeaders::get().request_headers)); + const auto& headers = + context.requestHeaders().value_or(*Http::StaticEmptyHeaders::get().request_headers); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.match_headers_individually")) { + return header_data_->matchesHeaders(headers); + } + return header_data_->matchesHeadersIndividually(headers); } ResponseFlagFilter::ResponseFlagFilter( diff --git a/source/common/access_log/access_log_impl.h b/source/common/access_log/access_log_impl.h index a312e9aaa20b1..5fb07922d0e20 100644 --- a/source/common/access_log/access_log_impl.h +++ b/source/common/access_log/access_log_impl.h @@ -188,7 +188,7 @@ class ResponseFlagFilter : public Filter { const StreamInfo::StreamInfo& info) const override; private: - std::vector configured_flags_{}; + std::vector configured_flags_; }; /** diff --git a/source/common/api/BUILD b/source/common/api/BUILD index c0908e70ff630..a71a71edfdcdc 100644 --- a/source/common/api/BUILD +++ b/source/common/api/BUILD @@ -15,6 +15,7 @@ envoy_cc_library( hdrs = ["api_impl.h"], deps = [ "//envoy/api:api_interface", + "//envoy/stats:store_interface", "//source/common/common:thread_lib", "//source/common/event:dispatcher_lib", "//source/common/network:socket_lib", diff --git a/source/common/api/api_impl.h b/source/common/api/api_impl.h index 3670ded65652c..96903d40e3ec3 100644 --- a/source/common/api/api_impl.h +++ b/source/common/api/api_impl.h @@ -8,6 +8,7 @@ #include "envoy/event/timer.h" #include "envoy/filesystem/filesystem.h" #include "envoy/network/socket.h" +#include "envoy/stats/store.h" #include "envoy/thread/thread.h" #include "source/common/stats/custom_stat_namespaces_impl.h" @@ -23,7 +24,7 @@ class Impl : public Api { Impl(Thread::ThreadFactory& thread_factory, Stats::Store& store, Event::TimeSystem& time_system, Filesystem::Instance& file_system, Random::RandomGenerator& random_generator, const envoy::config::bootstrap::v3::Bootstrap& bootstrap, - const ProcessContextOptRef& process_context = absl::nullopt, + const ProcessContextOptRef& process_context = std::nullopt, Buffer::WatermarkFactorySharedPtr watermark_factory = nullptr); // Api::Api diff --git a/source/common/api/posix/os_sys_calls_impl.cc b/source/common/api/posix/os_sys_calls_impl.cc index 8548a12039143..f8631eee19f27 100644 --- a/source/common/api/posix/os_sys_calls_impl.cc +++ b/source/common/api/posix/os_sys_calls_impl.cc @@ -1,12 +1,18 @@ #include "source/common/api/os_sys_calls_impl.h" +#include #include +#include #include #include #include #include +#if defined(__linux__) +#include +#endif + #include "envoy/network/socket.h" #include "source/common/network/address_impl.h" @@ -182,6 +188,43 @@ bool OsSysCallsImpl::supportsMptcp() const { #endif } +bool OsSysCallsImpl::supportsReusePortBpfCpuSteering() const { +#if !defined(SO_ATTACH_REUSEPORT_CBPF) || !defined(__linux__) + return false; +#else + static const bool is_supported = [] { + bool result = false; + const int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP); + if (fd >= 0) { + // Steer to the worker socket selected by the receiving CPU. + sock_filter filter[] = { + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, static_cast(SKF_AD_OFF + SKF_AD_CPU)), + BPF_STMT(BPF_ALU | BPF_MOD | BPF_K, 1), + BPF_STMT(BPF_RET | BPF_A, 0), + }; + sock_fprog prog{}; + prog.len = sizeof(filter) / sizeof(filter[0]); + prog.filter = filter; + // Bind and listen on an ephemeral loopback port so the probe attaches the program on the same + // socket state the real path uses. + int one = 1; + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (::setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) == 0 && + ::bind(fd, reinterpret_cast(&addr), sizeof(addr)) == 0 && + ::listen(fd, 1) == 0) { + result = ::setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, &prog, sizeof(prog)) == 0; + } + ::close(fd); + } + return result; + }(); + return is_supported; +#endif +} + SysCallIntResult OsSysCallsImpl::ftruncate(int fd, off_t length) { const int rc = ::ftruncate(fd, length); return {rc, rc != -1 ? 0 : errno}; diff --git a/source/common/api/posix/os_sys_calls_impl.h b/source/common/api/posix/os_sys_calls_impl.h index 4faf30d02f463..5ee3059e89a06 100644 --- a/source/common/api/posix/os_sys_calls_impl.h +++ b/source/common/api/posix/os_sys_calls_impl.h @@ -33,6 +33,7 @@ class OsSysCallsImpl : public OsSysCalls { bool supportsUdpGso() const override; bool supportsIpTransparent(Network::Address::IpVersion version) const override; bool supportsMptcp() const override; + bool supportsReusePortBpfCpuSteering() const override; SysCallIntResult close(os_fd_t fd) override; SysCallIntResult ftruncate(int fd, off_t length) override; SysCallPtrResult mmap(void* addr, size_t length, int prot, int flags, int fd, diff --git a/source/common/api/win32/os_sys_calls_impl.cc b/source/common/api/win32/os_sys_calls_impl.cc index 6f38a06858e5f..1e932c27bb284 100644 --- a/source/common/api/win32/os_sys_calls_impl.cc +++ b/source/common/api/win32/os_sys_calls_impl.cc @@ -222,6 +222,11 @@ bool OsSysCallsImpl::supportsMptcp() const { return false; } +bool OsSysCallsImpl::supportsReusePortBpfCpuSteering() const { + // Windows doesn't support it. + return false; +} + SysCallIntResult OsSysCallsImpl::ftruncate(int fd, off_t length) { const int rc = ::_chsize_s(fd, length); return {rc, rc == 0 ? 0 : errno}; diff --git a/source/common/api/win32/os_sys_calls_impl.h b/source/common/api/win32/os_sys_calls_impl.h index 91afbe18879fb..97d649c557499 100644 --- a/source/common/api/win32/os_sys_calls_impl.h +++ b/source/common/api/win32/os_sys_calls_impl.h @@ -34,6 +34,7 @@ class OsSysCallsImpl : public OsSysCalls { bool supportsUdpGso() const override; bool supportsIpTransparent(Network::Address::IpVersion version) const override; bool supportsMptcp() const override; + bool supportsReusePortBpfCpuSteering() const override; SysCallIntResult close(os_fd_t fd) override; SysCallIntResult ftruncate(int fd, off_t length) override; SysCallPtrResult mmap(void* addr, size_t length, int prot, int flags, int fd, diff --git a/source/common/buffer/buffer_impl.cc b/source/common/buffer/buffer_impl.cc index 8cda4e1777a45..7f3fca648b0d0 100644 --- a/source/common/buffer/buffer_impl.cc +++ b/source/common/buffer/buffer_impl.cc @@ -204,7 +204,7 @@ void OwnedImpl::drainImpl(uint64_t size) { } } -RawSliceVector OwnedImpl::getRawSlices(absl::optional max_slices) const { +RawSliceVector OwnedImpl::getRawSlices(std::optional max_slices) const { uint64_t max_out = slices_.size(); if (max_slices.has_value()) { max_out = std::min(max_out, max_slices.value()); @@ -590,7 +590,7 @@ bool OwnedImpl::startsWith(absl::string_view data) const { return false; } - if (data.length() == 0) { + if (data.empty()) { return true; } diff --git a/source/common/buffer/buffer_impl.h b/source/common/buffer/buffer_impl.h index 72cca4ba9b1a2..87366194ea887 100644 --- a/source/common/buffer/buffer_impl.h +++ b/source/common/buffer/buffer_impl.h @@ -40,7 +40,7 @@ class Slice { using StoragePtr = std::unique_ptr; struct SizedStorage { - StoragePtr mem_{}; + StoragePtr mem_; size_t len_{}; }; @@ -661,7 +661,7 @@ class OwnedImpl : public LibEventInstance { uint64_t copyOutToSlices(uint64_t size, Buffer::RawSlice* slices, uint64_t num_slice) const override; void drain(uint64_t size) override; - RawSliceVector getRawSlices(absl::optional max_slices = absl::nullopt) const override; + RawSliceVector getRawSlices(std::optional max_slices = std::nullopt) const override; RawSlice frontSlice() const override; SliceDataPtr extractMutableFrontSlice() override; uint64_t length() const override; diff --git a/source/common/buffer/watermark_buffer.cc b/source/common/buffer/watermark_buffer.cc index 47bf4d5a706ad..d7538d9cef304 100644 --- a/source/common/buffer/watermark_buffer.cc +++ b/source/common/buffer/watermark_buffer.cc @@ -169,8 +169,8 @@ WatermarkBufferFactory::createAccount(Http::StreamResetHandler& reset_handler) { } void WatermarkBufferFactory::updateAccountClass(const BufferMemoryAccountSharedPtr& account, - absl::optional current_class, - absl::optional new_class) { + std::optional current_class, + std::optional new_class) { ASSERT(current_class != new_class, "Expected the current_class and new_class to be different"); if (!current_class.has_value()) { @@ -193,7 +193,7 @@ void WatermarkBufferFactory::updateAccountClass(const BufferMemoryAccountSharedP } void WatermarkBufferFactory::unregisterAccount(const BufferMemoryAccountSharedPtr& account, - absl::optional current_class) { + std::optional current_class) { if (current_class.has_value()) { ASSERT(size_class_account_sets_[current_class.value()].contains(account)); size_class_account_sets_[current_class.value()].erase(account); @@ -264,7 +264,7 @@ BufferMemoryAccountImpl::createAccount(WatermarkBufferFactory* factory, return account; } -absl::optional BufferMemoryAccountImpl::balanceToClassIndex() { +std::optional BufferMemoryAccountImpl::balanceToClassIndex() { const uint64_t shifted_balance = buffer_memory_allocated_ >> factory_->bitshift(); if (shifted_balance == 0) { diff --git a/source/common/buffer/watermark_buffer.h b/source/common/buffer/watermark_buffer.h index 18c5df1cb0007..1e8e60f49b7d1 100644 --- a/source/common/buffer/watermark_buffer.h +++ b/source/common/buffer/watermark_buffer.h @@ -9,6 +9,8 @@ #include "source/common/buffer/buffer_impl.h" +#include "absl/functional/any_invocable.h" + namespace Envoy { namespace Buffer { @@ -21,11 +23,12 @@ namespace Buffer { // It is only called on the first time the buffer overflows. class WatermarkBuffer : public OwnedImpl { public: - WatermarkBuffer(std::function below_low_watermark, - std::function above_high_watermark, - std::function above_overflow_watermark) - : below_low_watermark_(below_low_watermark), above_high_watermark_(above_high_watermark), - above_overflow_watermark_(above_overflow_watermark) {} + WatermarkBuffer(absl::AnyInvocable below_low_watermark, + absl::AnyInvocable above_high_watermark, + absl::AnyInvocable above_overflow_watermark) + : below_low_watermark_(std::move(below_low_watermark)), + above_high_watermark_(std::move(above_high_watermark)), + above_overflow_watermark_(std::move(above_overflow_watermark)) {} // Override all functions from Instance which can result in changing the size // of the underlying buffer. @@ -61,9 +64,9 @@ class WatermarkBuffer : public OwnedImpl { void commit(uint64_t length, absl::Span slices, ReservationSlicesOwnerPtr slices_owner) override; - std::function below_low_watermark_; - std::function above_high_watermark_; - std::function above_overflow_watermark_; + absl::AnyInvocable below_low_watermark_; + absl::AnyInvocable above_high_watermark_; + absl::AnyInvocable above_overflow_watermark_; // Used for enforcing buffer limits (off by default). If these are set to non-zero by a call to // setWatermarks() the watermark callbacks will be called as described above. @@ -139,12 +142,12 @@ class BufferMemoryAccountImpl : public BufferMemoryAccount { // This can differ with current_bucket_idx_ if buffer_memory_allocated_ was // just modified. // Returned class index, if present, is in the range [0, NUM_MEMORY_CLASSES_). - absl::optional balanceToClassIndex(); + std::optional balanceToClassIndex(); void updateAccountClass(); uint64_t buffer_memory_allocated_ = 0; // Current bucket index where the account is being tracked in. - absl::optional current_bucket_idx_{}; + std::optional current_bucket_idx_; WatermarkBufferFactory* factory_ = nullptr; @@ -194,11 +197,12 @@ class WatermarkBufferFactory : public WatermarkFactory { // Buffer::WatermarkFactory ~WatermarkBufferFactory() override; - InstancePtr createBuffer(std::function below_low_watermark, - std::function above_high_watermark, - std::function above_overflow_watermark) override { - return std::make_unique(below_low_watermark, above_high_watermark, - above_overflow_watermark); + InstancePtr createBuffer(absl::AnyInvocable below_low_watermark, + absl::AnyInvocable above_high_watermark, + absl::AnyInvocable above_overflow_watermark) override { + return std::make_unique(std::move(below_low_watermark), + std::move(above_high_watermark), + std::move(above_overflow_watermark)); } BufferMemoryAccountSharedPtr createAccount(Http::StreamResetHandler& reset_handler) override; @@ -207,14 +211,13 @@ class WatermarkBufferFactory : public WatermarkFactory { // Called by BufferMemoryAccountImpls created by the factory on account class // updated. void updateAccountClass(const BufferMemoryAccountSharedPtr& account, - absl::optional current_class, - absl::optional new_class); + std::optional current_class, std::optional new_class); uint32_t bitshift() const { return bitshift_; } // Unregister a buffer memory account. virtual void unregisterAccount(const BufferMemoryAccountSharedPtr& account, - absl::optional current_class); + std::optional current_class); protected: // Enable subclasses to inspect the mapping. diff --git a/source/common/common/BUILD b/source/common/common/BUILD index ebc894f407bab..155bace5e38be 100644 --- a/source/common/common/BUILD +++ b/source/common/common/BUILD @@ -47,6 +47,15 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "cpu_affinity_lib", + srcs = ["cpu_affinity.cc"], + hdrs = ["cpu_affinity.h"], + deps = [ + "//source/common/api:os_sys_calls_lib", + ], +) + envoy_cc_library( name = "debug_recursion_checker_lib", hdrs = ["debug_recursion_checker.h"], @@ -222,6 +231,11 @@ envoy_cc_library( ":macros", ":non_copyable", "//source/common/protobuf", + # Depend on the header-only interface, not the implementation lib, so that every commit to + # main does not invalidate the build cache of minimal_logger_lib (and everything that + # transitively depends on it). The implementation is linked by the final binary via + # version_linkstamp (production) / test_version_linkstamp (tests). + "//source/common/version:version_string_interface_lib", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/synchronization", ] + select({ @@ -328,7 +342,6 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/http:path_utility_lib", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", ], @@ -342,6 +355,7 @@ envoy_cc_library( "//envoy/common:exception_lib", "//envoy/common:matchers_interface", "//envoy/common:pure_lib", + "//source/common/matcher:address_matcher_lib", "//source/common/network:cidr_range_lib", "@abseil-cpp//absl/status:statusor", ], diff --git a/source/common/common/base64.cc b/source/common/common/base64.cc index 7ca2109fbb502..833ddfc9eb61d 100644 --- a/source/common/common/base64.cc +++ b/source/common/common/base64.cc @@ -3,21 +3,15 @@ #include #include -#include "source/common/common/assert.h" #include "source/common/common/empty_string.h" -#include "absl/container/fixed_array.h" +#include "absl/strings/escaping.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" namespace Envoy { namespace { -// clang-format off -constexpr char CHAR_TABLE[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -// Conversion table is taken from -// https://opensource.apple.com/source/QuickTimeStreamingServer/QuickTimeStreamingServer-452/CommonUtilitiesLib/base64.c constexpr unsigned char REVERSE_LOOKUP_TABLE[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, @@ -31,11 +25,6 @@ constexpr unsigned char REVERSE_LOOKUP_TABLE[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64}; -// The base64url tables are copied from above and modified based on table in -// https://tools.ietf.org/html/rfc4648#section-5 -constexpr char URL_CHAR_TABLE[] = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; - constexpr unsigned char URL_REVERSE_LOOKUP_TABLE[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, @@ -48,99 +37,53 @@ constexpr unsigned char URL_REVERSE_LOOKUP_TABLE[256] = { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64}; -// clang-format on -inline bool decodeBase(const uint8_t cur_char, uint64_t pos, std::string& ret, - const unsigned char* const reverse_lookup_table) { - const unsigned char c = reverse_lookup_table[static_cast(cur_char)]; - if (c == 64) { - // Invalid character - return false; +std::string decodeHelper(absl::string_view input, const unsigned char* lookup_table, + bool (*unescape_fn)(absl::string_view, std::string*)) { + if (input.empty()) { + return EMPTY_STRING; } - switch (pos % 4) { - case 0: - ret.push_back(c << 2); - break; - case 1: - ret.back() |= c >> 4; - ret.push_back(c << 4); - break; - case 2: - ret.back() |= c >> 2; - ret.push_back(c << 6); - break; - case 3: - ret.back() |= c; - break; - } - return true; -} + // Strip up to two trailing '=' characters. + const size_t n = input.length(); + const size_t count = (input[n - 1] == '=') + (n >= 2 && input[n - 2] == '='); + const size_t real_chars = n - count; -inline bool decodeLast(const uint8_t cur_char, uint64_t pos, std::string& ret, - const unsigned char* const reverse_lookup_table) { - const unsigned char c = reverse_lookup_table[static_cast(cur_char)]; - if (c == 64) { - // Invalid character - return false; + // A base64 string with length % 4 == 1 is structurally invalid + if (real_chars % 4 == 1) { + return EMPTY_STRING; } - switch (pos % 4) { - case 0: - return false; - case 1: - ret.back() |= c >> 4; - return (c & 0b1111) == 0; - case 2: - ret.back() |= c >> 2; - return (c & 0b11) == 0; - case 3: - ret.back() |= c; - break; + // Validate the trailing bits of the last real character. + if (real_chars > 0) { + const uint8_t last_value = lookup_table[static_cast(input[real_chars - 1])]; + if (last_value == 64) { + return EMPTY_STRING; + } + // Base64 groups input into 4 character blocks. Each character represents 6 bits. + // A valid encoded string can have trailing characters where the unused lower bits must be 0. + // This code checks whether the unused bits are 0. If they are not, the input is invalid. + if (real_chars % 4 == 2 && (last_value & 0x0F) != 0) { + return EMPTY_STRING; + } + if (real_chars % 4 == 3 && (last_value & 0x03) != 0) { + return EMPTY_STRING; + } } - return true; -} -inline void encodeBase(const uint8_t cur_char, uint64_t pos, uint8_t& next_c, std::string& ret, - const char* const char_table) { - switch (pos % 3) { - case 0: - ret.push_back(char_table[cur_char >> 2]); - next_c = (cur_char & 0x03) << 4; - break; - case 1: - ret.push_back(char_table[next_c | (cur_char >> 4)]); - next_c = (cur_char & 0x0f) << 2; - break; - case 2: - ret.push_back(char_table[next_c | (cur_char >> 6)]); - ret.push_back(char_table[cur_char & 0x3f]); - next_c = 0; - break; + std::string ret; + if (!unescape_fn(input.substr(0, real_chars), &ret)) { + return EMPTY_STRING; } -} -inline void encodeLast(uint64_t pos, uint8_t last_char, std::string& ret, - const char* const char_table, bool add_padding) { - switch (pos % 3) { - case 1: - ret.push_back(char_table[last_char]); - if (add_padding) { - ret.push_back('='); - ret.push_back('='); - } - break; - case 2: - ret.push_back(char_table[last_char]); - if (add_padding) { - ret.push_back('='); - } - break; - default: - break; + // If abseil decodes the wrong number of bytes, we must reject the entire input. + size_t expected_length = real_chars * 3 / 4; + if (ret.length() != expected_length) { + return EMPTY_STRING; } -} + return ret; +} } // namespace std::string Base64::decode(absl::string_view input) { @@ -151,66 +94,25 @@ std::string Base64::decode(absl::string_view input) { } std::string Base64::decodeWithoutPadding(absl::string_view input) { - if (input.empty()) { - return EMPTY_STRING; - } - - // At most last two chars can be '='. - size_t n = input.length(); - if (input[n - 1] == '=') { - n--; - if (n > 0 && input[n - 1] == '=') { - n--; - } - } - // Last position before "valid" padding character. - uint64_t last = n - 1; - // Determine output length. - size_t max_length = (n + 3) / 4 * 3; - if (n % 4 == 3) { - max_length -= 1; - } - if (n % 4 == 2) { - max_length -= 2; - } - - std::string ret; - ret.reserve(max_length); - for (uint64_t i = 0; i < last; ++i) { - if (!decodeBase(input[i], i, ret, REVERSE_LOOKUP_TABLE)) { - return EMPTY_STRING; - } - } - - if (!decodeLast(input[last], last, ret, REVERSE_LOOKUP_TABLE)) { - return EMPTY_STRING; - } - - ASSERT(ret.size() == max_length); - return ret; + return decodeHelper(input, REVERSE_LOOKUP_TABLE, absl::Base64Unescape); } std::string Base64::encode(const Buffer::Instance& buffer, uint64_t length) { - uint64_t output_length = (std::min(length, buffer.length()) + 2) / 3 * 4; + const uint64_t encode_length = std::min(length, buffer.length()); + if (encode_length == 0) { + return EMPTY_STRING; + } std::string ret; - ret.reserve(output_length); - - uint64_t j = 0; - uint8_t next_c = 0; - for (const Buffer::RawSlice& slice : buffer.getRawSlices()) { - const uint8_t* slice_mem = static_cast(slice.mem_); - - for (uint64_t i = 0; i < slice.len_ && j < length; ++i, ++j) { - encodeBase(slice_mem[i], j, next_c, ret, CHAR_TABLE); - } - - if (j == length) { - break; - } + const auto slices = buffer.getRawSlices(); + if (slices.size() == 1 && slices[0].len_ >= encode_length) { + absl::Base64Escape(absl::string_view(static_cast(slices[0].mem_), encode_length), + &ret); + } else { + std::string tmp; + tmp.resize(encode_length); + buffer.copyOut(0, encode_length, tmp.data()); + absl::Base64Escape(tmp, &ret); } - - encodeLast(j, next_c, ret, CHAR_TABLE, true); - return ret; } @@ -221,19 +123,15 @@ std::string Base64::encode(const char* input, uint64_t length) { } std::string Base64::encode(const char* input, uint64_t length, bool add_padding) { - uint64_t output_length = (length + 2) / 3 * 4; std::string ret; - ret.reserve(output_length); + absl::Base64Escape(absl::string_view(input, length), &ret); - uint64_t pos = 0; - uint8_t next_c = 0; - - for (uint64_t i = 0; i < length; ++i) { - encodeBase(input[i], pos++, next_c, ret, CHAR_TABLE); + if (!add_padding) { + // Remove trailing '=' + const size_t n = ret.size(); + const size_t count = (n >= 1 && ret[n - 1] == '=') + (n >= 2 && ret[n - 2] == '='); + ret.resize(n - count); } - - encodeLast(pos, next_c, ret, CHAR_TABLE, add_padding); - return ret; } @@ -245,41 +143,20 @@ void Base64::completePadding(std::string& encoded) { } std::string Base64Url::decode(absl::string_view input) { - if (input.empty()) { - return EMPTY_STRING; - } - - std::string ret; - ret.reserve(input.length() / 4 * 3 + 3); - - uint64_t last = input.length() - 1; - for (uint64_t i = 0; i < last; ++i) { - if (!decodeBase(input[i], i, ret, URL_REVERSE_LOOKUP_TABLE)) { + // URL decoding does not accept any padding + if (!input.empty()) { + const uint8_t last_value = + URL_REVERSE_LOOKUP_TABLE[static_cast(input[input.size() - 1])]; + if (last_value == 64) { return EMPTY_STRING; } } - - if (!decodeLast(input[last], last, ret, URL_REVERSE_LOOKUP_TABLE)) { - return EMPTY_STRING; - } - - return ret; + return decodeHelper(input, URL_REVERSE_LOOKUP_TABLE, absl::WebSafeBase64Unescape); } std::string Base64Url::encode(const char* input, uint64_t length) { - uint64_t output_length = (length + 2) / 3 * 4; std::string ret; - ret.reserve(output_length); - - uint64_t pos = 0; - uint8_t next_c = 0; - - for (uint64_t i = 0; i < length; ++i) { - encodeBase(input[i], pos++, next_c, ret, URL_CHAR_TABLE); - } - - encodeLast(pos, next_c, ret, URL_CHAR_TABLE, false); - + absl::WebSafeBase64Escape(absl::string_view(input, length), &ret); return ret; } diff --git a/source/common/common/basic_resource_impl.h b/source/common/common/basic_resource_impl.h index 8fedbb4ad8013..93b55f98537a3 100644 --- a/source/common/common/basic_resource_impl.h +++ b/source/common/common/basic_resource_impl.h @@ -1,14 +1,13 @@ #pragma once #include +#include #include "envoy/common/resource.h" #include "envoy/runtime/runtime.h" #include "source/common/common/assert.h" -#include "absl/types/optional.h" - namespace Envoy { /** @@ -49,12 +48,12 @@ class BasicResourceLimitImpl : public ResourceLimit { void resetMax() { max_ = std::numeric_limits::max(); } protected: - std::atomic current_{}; + std::atomic current_{0}; private: uint64_t max_; Runtime::Loader* runtime_{nullptr}; - const absl::optional runtime_key_; + const std::optional runtime_key_; }; } // namespace Envoy diff --git a/source/common/common/cpu_affinity.cc b/source/common/common/cpu_affinity.cc new file mode 100644 index 0000000000000..c3345bfe224b8 --- /dev/null +++ b/source/common/common/cpu_affinity.cc @@ -0,0 +1,45 @@ +#include "source/common/common/cpu_affinity.h" + +#if defined(__linux__) +#include +#include + +#include "source/common/api/os_sys_calls_impl_linux.h" +#endif + +namespace Envoy { +namespace Thread { + +std::vector cpuAffinitySet() { +#if defined(__linux__) + cpu_set_t mask; + CPU_ZERO(&mask); + // Read the process mask via `getpid()`. The worker threads inherit it at startup. + const Api::SysCallIntResult result = + Api::LinuxOsSysCallsSingleton::get().sched_getaffinity(getpid(), sizeof(cpu_set_t), &mask); + if (result.return_value_ != 0) { + return {}; + } + std::vector cpus; + for (uint32_t cpu = 0; cpu < CPU_SETSIZE; cpu++) { + if (CPU_ISSET(cpu, &mask)) { + cpus.push_back(cpu); + } + } + return cpus; +#else + return {}; +#endif +} + +std::vector workerCpuAssignment(uint32_t worker_count) { + std::vector cpus = cpuAffinitySet(); + if (worker_count == 0 || cpus.size() < worker_count) { + return {}; + } + cpus.resize(worker_count); + return cpus; +} + +} // namespace Thread +} // namespace Envoy diff --git a/source/common/common/cpu_affinity.h b/source/common/common/cpu_affinity.h new file mode 100644 index 0000000000000..76e1947922e05 --- /dev/null +++ b/source/common/common/cpu_affinity.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace Envoy { +namespace Thread { + +// Returns the CPUs in the process affinity mask in ascending order. Empty when the platform has no +// affinity support or the query fails. +std::vector cpuAffinitySet(); + +// Returns the first `worker_count` CPUs of the process affinity mask, used to pin worker i to entry +// i. Empty when fewer CPUs are available than workers, which disables worker CPU pinning. +std::vector workerCpuAssignment(uint32_t worker_count); + +} // namespace Thread +} // namespace Envoy diff --git a/source/common/common/filter_state_object_matchers.cc b/source/common/common/filter_state_object_matchers.cc index 1a6ea0e751f38..0334270fab1dd 100644 --- a/source/common/common/filter_state_object_matchers.cc +++ b/source/common/common/filter_state_object_matchers.cc @@ -4,17 +4,12 @@ #include "envoy/network/address.h" #include "envoy/stream_info/filter_state.h" -#include "source/common/network/cidr_range.h" - -#include "absl/status/statusor.h" -#include "xds/core/v3/cidr.pb.h" - namespace Envoy { namespace Matchers { FilterStateIpRangeMatcher::FilterStateIpRangeMatcher( std::unique_ptr&& ip_list, bool invert_match) - : ip_list_(std::move(ip_list)), invert_match_(invert_match) {} + : address_matcher_(std::move(ip_list), invert_match) {} bool FilterStateIpRangeMatcher::match(const StreamInfo::FilterState::Object& object) const { const Network::Address::InstanceAccessor* ip = @@ -22,8 +17,7 @@ bool FilterStateIpRangeMatcher::match(const StreamInfo::FilterState::Object& obj if (ip == nullptr) { return false; } - const bool matches = ip_list_->contains(*ip->getIp()); - return invert_match_ ? !matches : matches; + return address_matcher_.match(*ip->getIp()); } FilterStateStringMatcher::FilterStateStringMatcher(StringMatcherPtr&& string_matcher) diff --git a/source/common/common/filter_state_object_matchers.h b/source/common/common/filter_state_object_matchers.h index c901bbd80e941..d293c83b534e4 100644 --- a/source/common/common/filter_state_object_matchers.h +++ b/source/common/common/filter_state_object_matchers.h @@ -6,6 +6,7 @@ #include "envoy/common/pure.h" #include "envoy/stream_info/filter_state.h" +#include "source/common/matcher/address_matcher.h" #include "source/common/network/cidr_range.h" namespace Envoy { @@ -26,8 +27,7 @@ class FilterStateIpRangeMatcher : public FilterStateObjectMatcher { bool match(const StreamInfo::FilterState::Object& object) const override; private: - std::unique_ptr ip_list_; - const bool invert_match_; + const Envoy::Matcher::AddressMatcher address_matcher_; }; class FilterStateStringMatcher : public FilterStateObjectMatcher { diff --git a/source/common/common/fine_grain_logger.cc b/source/common/common/fine_grain_logger.cc index 7ef40f07ec31e..c5c7ff70b8d9d 100644 --- a/source/common/common/fine_grain_logger.cc +++ b/source/common/common/fine_grain_logger.cc @@ -1,11 +1,13 @@ #include "source/common/common/fine_grain_logger.h" #include +#include #include #include #include "source/common/common/logger.h" +#include "absl/strings/match.h" #include "absl/strings/str_join.h" using spdlog::level::level_enum; @@ -36,14 +38,49 @@ SpdLoggerSharedPtr FineGrainLogContext::getFineGrainLogEntry(absl::string_view k return nullptr; } +SpdLoggerSharedPtr FineGrainLogContext::getFineGrainLogEntryForFlush(absl::string_view file, + absl::string_view name) + ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { + if (name.empty()) { + return getFineGrainLogEntry(file); + } + return getFineGrainLogEntry(absl::StrCat(file, ":", name)); +} spdlog::level::level_enum FineGrainLogContext::getVerbosityDefaultLevel() const { absl::ReaderMutexLock l(fine_grain_log_lock_); return verbosity_default_level_; } -void FineGrainLogContext::initFineGrainLogger(const std::string& key, - std::atomic& logger) +bool FineGrainLogContext::checkFineGrainLogger(spdlog::logger* logger, absl::string_view file, + absl::string_view name) { + absl::string_view logger_name = logger->name(); + // Fast path for static strings: the pointer address might match if they are string literals. + // spdlog logger name is usually a std::string, so logger_name.data() is not the same as + // file.data(). + + if (name.empty()) { + return logger_name == file; + } + + // Common case: logger_name was created as absl::StrCat(file, ":", name) + if (logger_name.size() != file.size() + 1 + name.size()) { + return false; + } + + const char* d = logger_name.data(); + return d[file.size()] == ':' && absl::StartsWith(logger_name, file) && + absl::EndsWith(logger_name, name); +} + +spdlog::logger* FineGrainLogContext::initFineGrainLogger(absl::string_view file, + absl::string_view name, + std::atomic& logger) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { + std::string key(file); + if (!name.empty()) { + absl::StrAppend(&key, ":", name); + } + absl::WriterMutexLock l(fine_grain_log_lock_); auto it = fine_grain_log_map_->find(key); spdlog::logger* target; @@ -53,6 +90,7 @@ void FineGrainLogContext::initFineGrainLogger(const std::string& key, target = it->second.get(); } logger.store(target, std::memory_order_release); + return target; } bool FineGrainLogContext::setFineGrainLogger(absl::string_view key, level_enum log_level) @@ -83,13 +121,17 @@ void FineGrainLogContext::setDefaultFineGrainLogLevelFormat(spdlog::level::level std::string FineGrainLogContext::listFineGrainLoggers() ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_) { absl::ReaderMutexLock l(fine_grain_log_lock_); - std::string info = - absl::StrJoin(*fine_grain_log_map_, "\n", [](std::string* out, const auto& log_pair) { - auto level_str_view = spdlog::level::to_string_view(log_pair.second->level()); - absl::StrAppend(out, " ", log_pair.first, ": ", - absl::string_view(level_str_view.data(), level_str_view.size())); - }); - return info; + std::vector lines; + for (const auto& log_pair : *fine_grain_log_map_) { + const auto [file, logger_name] = parseKey(log_pair.first); + if (!logger_name.empty()) { + continue; + } + auto level_str_view = spdlog::level::to_string_view(log_pair.second->level()); + lines.push_back(absl::StrCat(" ", file, ": ", + absl::string_view(level_str_view.data(), level_str_view.size()))); + } + return absl::StrJoin(lines, "\n"); } void FineGrainLogContext::setAllFineGrainLoggers(spdlog::level::level_enum level) @@ -177,18 +219,18 @@ void FineGrainLogContext::updateVerbosityDefaultLevel(level_enum level) { setAllFineGrainLoggers(level); } -void FineGrainLogContext::updateVerbositySetting( - const std::vector>& updates) { +void FineGrainLogContext::updateVerbositySetting(const std::vector& updates) { absl::WriterMutexLock ul(fine_grain_log_lock_); verbosity_update_info_.clear(); - for (const auto& [glob, level] : updates) { - if (level < kLogLevelMin || level > kLogLevelMax) { - printf( - "The log level: %d for glob: %s is out of scope, and it should be in [0, 6]. Skipping.", - level, std::string(glob).c_str()); + for (const auto& update : updates) { + if (update.level < kLogLevelMin || update.level > kLogLevelMax) { + printf("The log level: %d for pattern: %s is out of scope, and it should be in [0, 6]. " + "Skipping.", + update.level, std::string(update.pattern).c_str()); continue; } - appendVerbosityLogUpdate(glob, static_cast(level)); + appendVerbosityLogUpdate(update.pattern, static_cast(update.level), + update.match_group_only); } for (const auto& [key, logger] : *fine_grain_log_map_) { @@ -197,33 +239,36 @@ void FineGrainLogContext::updateVerbositySetting( } void FineGrainLogContext::appendVerbosityLogUpdate(absl::string_view update_pattern, - level_enum log_level) { + level_enum log_level, bool match_group_only) { for (const auto& info : verbosity_update_info_) { - if (safeFileNameMatch(info.update_pattern, update_pattern)) { + if (info.match_group_only == match_group_only && + safeFileNameMatch(info.update_pattern, update_pattern)) { // This is a memory optimization to avoid storing patterns that will never // match due to exit early semantics. return; } } bool update_is_path = update_pattern.find('/') != update_pattern.npos; - verbosity_update_info_.emplace_back(std::string(update_pattern), update_is_path, log_level); + verbosity_update_info_.emplace_back(std::string(update_pattern), update_is_path, match_group_only, + log_level); } - -level_enum FineGrainLogContext::getLogLevel(absl::string_view file) const { +level_enum FineGrainLogContext::getLogLevel(absl::string_view key) const { if (verbosity_update_info_.empty()) { return verbosity_default_level_; } + const auto [file, logger_name] = parseKey(key); + // Get basename for file. - absl::string_view basename = file; + absl::string_view file_basename = file; { - const size_t sep = basename.rfind('/'); - if (sep != basename.npos) { - basename.remove_prefix(sep + 1); + const size_t sep = file_basename.rfind('/'); + if (sep != file_basename.npos) { + file_basename.remove_prefix(sep + 1); } } - absl::string_view stem_basename = basename; + absl::string_view stem_basename = file_basename; { const size_t sep = stem_basename.find('.'); if (sep != stem_basename.npos) { @@ -231,19 +276,50 @@ level_enum FineGrainLogContext::getLogLevel(absl::string_view file) const { } } for (const auto& info : verbosity_update_info_) { + if (info.match_group_only) { + if (!logger_name.empty() && info.update_pattern == logger_name) { + return info.log_level; + } + continue; + } + if (info.update_is_path) { // If there are any slashes in the pattern, try to match the full path name. if (safeFileNameMatch(info.update_pattern, file)) { return info.log_level; } - } else if (safeFileNameMatch(info.update_pattern, stem_basename)) { - return info.log_level; + } else { + if (safeFileNameMatch(info.update_pattern, stem_basename)) { + return info.log_level; + } + if (!logger_name.empty() && info.update_pattern == logger_name) { + return info.log_level; + } } } return verbosity_default_level_; } +std::pair +FineGrainLogContext::parseKey(absl::string_view key) { + absl::string_view file = key; + absl::string_view name; + size_t colon = key.rfind(':'); +#ifdef _WIN32 + // On Windows, a colon at index 1 is likely a drive letter (e.g., C:\path). + if (colon == 1 && key.size() > 1 && + ((key[0] >= 'a' && key[0] <= 'z') || (key[0] >= 'A' && key[0] <= 'Z'))) { + colon = absl::string_view::npos; + } +#endif + if (colon != absl::string_view::npos) { + name = key.substr(colon + 1); + file = key.substr(0, colon); + } + return {file, name}; +} + FineGrainLogContext& getFineGrainLogContext() { MUTABLE_CONSTRUCT_ON_FIRST_USE(FineGrainLogContext); } diff --git a/source/common/common/fine_grain_logger.h b/source/common/common/fine_grain_logger.h index 9c5fb75c60562..86d53711c0a77 100644 --- a/source/common/common/fine_grain_logger.h +++ b/source/common/common/fine_grain_logger.h @@ -24,13 +24,14 @@ inline const char* kDefaultFineGrainLogFormat = "[%Y-%m-%d %T.%e][%t][%l] [%g:%# */ struct VerbosityLogUpdateInfo final { const std::string update_pattern; - const bool update_is_path; // i.e. it contains a path separator. + const bool update_is_path; // i.e. it contains a path separator. + const bool match_group_only; // i.e. it only matches against logger group names. const spdlog::level::level_enum log_level; VerbosityLogUpdateInfo(absl::string_view update_pattern, bool update_is_path, - spdlog::level::level_enum log_level) + bool match_group_only, spdlog::level::level_enum log_level) : update_pattern(std::string(update_pattern)), update_is_path(update_is_path), - log_level(log_level) {} + match_group_only(match_group_only), log_level(log_level) {} }; /** @@ -45,6 +46,13 @@ class FineGrainLogContext { SpdLoggerSharedPtr getFineGrainLogEntry(absl::string_view key) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); + /** + * Gets a logger from map given a file and name. + */ + SpdLoggerSharedPtr getFineGrainLogEntryForFlush(absl::string_view file, + absl::string_view name = "") + ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); + /** * Gets the default verbosity log level. */ @@ -55,7 +63,8 @@ class FineGrainLogContext { * Initializes Fine-Grain Logger, gets log level from setting vector, and registers it in global * map if not done. */ - void initFineGrainLogger(const std::string& key, std::atomic& logger) + spdlog::logger* initFineGrainLogger(absl::string_view file, absl::string_view logger_name, + std::atomic& logger) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); /** @@ -89,12 +98,23 @@ class FineGrainLogContext { */ FineGrainLogLevelMap getAllFineGrainLogLevelsForTest() ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); + /** + * Data struct for verbosity updates. + */ + struct VerbosityUpdate { + absl::string_view pattern; + int level; + bool match_group_only = false; + }; + /** * Updates all loggers based on the verbosity updates <(file, level) ...>. * It supports file basename and glob "*" and "?" pattern, eg. ("foo", 2), ("foo/b*", 3) * Patterns including a slash character are matched against full path names, while those * without are matched against base names (by removing one suffix) only. * + * If match_group_only is true, the pattern is only matched against the logger group name. + * * It will store the current verbosity updates and clear all previous modifications for * future check when initializing a new logger. * @@ -103,7 +123,7 @@ class FineGrainLogContext { * * Files which do not match any pattern use the default verbosity log level. */ - void updateVerbositySetting(const std::vector>& updates) + void updateVerbositySetting(const std::vector& updates) ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); /** @@ -124,6 +144,12 @@ class FineGrainLogContext { */ static bool safeFileNameMatch(absl::string_view pattern, absl::string_view str); + /** + * Check if a logger's name matches the given file and logger name. + */ + bool checkFineGrainLogger(spdlog::logger* logger, absl::string_view file, absl::string_view name) + ABSL_LOCKS_EXCLUDED(fine_grain_log_lock_); + /** * Remove a fine grain log entry for testing only. */ @@ -150,14 +176,14 @@ class FineGrainLogContext { * Append verbosity level updates to the VerbosityLogUpdateInfo vector. */ void appendVerbosityLogUpdate(absl::string_view update_pattern, - spdlog::level::level_enum log_level) + spdlog::level::level_enum log_level, bool match_group_only) ABSL_EXCLUSIVE_LOCKS_REQUIRED(fine_grain_log_lock_); /** - * Returns the current log level of `file`. Default log level is used if there is no + * Returns the current log level of `key`. Default log level is used if there is no * match in verbosity_update_info_. */ - spdlog::level::level_enum getLogLevel(absl::string_view file) const + spdlog::level::level_enum getLogLevel(absl::string_view key) const ABSL_SHARED_LOCKS_REQUIRED(fine_grain_log_lock_); /** @@ -166,6 +192,11 @@ class FineGrainLogContext { */ mutable absl::Mutex fine_grain_log_lock_; + /** + * Parses a logger key into its file and name components. + */ + static std::pair parseKey(absl::string_view key); + /** * Map that stores pairs, key can be the file name. */ @@ -194,23 +225,52 @@ FineGrainLogContext& getFineGrainLogContext(); * The local pointer is used to avoid another load() when logging. Here we use * spdlog::logger* as atomic is a C++20 feature. */ -#define FINE_GRAIN_LOGGER() \ - ([]() -> spdlog::logger* { \ +#define FINE_GRAIN_LOGGER(NAME) \ + ([&](absl::string_view name) -> spdlog::logger* { \ static std::atomic flogger{nullptr}; \ spdlog::logger* local_flogger = flogger.load(std::memory_order_acquire); \ - if (!local_flogger) { \ - ::Envoy::getFineGrainLogContext().initFineGrainLogger(__FILE__, flogger); \ - local_flogger = flogger.load(std::memory_order_acquire); \ + \ + if (__builtin_constant_p(NAME)) { \ + if (ABSL_PREDICT_FALSE(!local_flogger)) { \ + local_flogger = \ + ::Envoy::getFineGrainLogContext().initFineGrainLogger(__FILE__, name, flogger); \ + } \ + } else { \ + static std::atomic cached_name_ptr{nullptr}; \ + const char* current_ptr = name.data(); \ + if (ABSL_PREDICT_FALSE(!local_flogger || \ + cached_name_ptr.load(std::memory_order_relaxed) != current_ptr)) { \ + if (!local_flogger || !::Envoy::getFineGrainLogContext().checkFineGrainLogger( \ + local_flogger, __FILE__, name)) { \ + local_flogger = \ + ::Envoy::getFineGrainLogContext().initFineGrainLogger(__FILE__, name, flogger); \ + } \ + if (local_flogger) { \ + cached_name_ptr.store(current_ptr, std::memory_order_relaxed); \ + } \ + } \ } \ return local_flogger; \ - }()) + }(NAME)) /** - * Macro for fine-grain logger to log. + * Macro for fine-grain logger to log without a group. */ #define FINE_GRAIN_LOG(LEVEL, ...) \ do { \ - spdlog::logger* local_flogger = FINE_GRAIN_LOGGER(); \ + spdlog::logger* local_flogger = FINE_GRAIN_LOGGER(""); \ + if (ENVOY_LOG_COMP_LEVEL(*local_flogger, LEVEL)) { \ + local_flogger->log(spdlog::source_loc{__FILE__, __LINE__, __func__}, \ + ENVOY_SPDLOG_LEVEL(LEVEL), __VA_ARGS__); \ + } \ + } while (0) + +/** + * Macro for fine-grain logger to log with a group. + */ +#define FINE_GRAIN_GROUP_LOG(LEVEL, NAME, ...) \ + do { \ + spdlog::logger* local_flogger = FINE_GRAIN_LOGGER(NAME); \ if (ENVOY_LOG_COMP_LEVEL(*local_flogger, LEVEL)) { \ local_flogger->log(spdlog::source_loc{__FILE__, __LINE__, __func__}, \ ENVOY_SPDLOG_LEVEL(LEVEL), __VA_ARGS__); \ @@ -221,22 +281,23 @@ FineGrainLogContext& getFineGrainLogContext(); * Convenient macro for connection log. */ #define FINE_GRAIN_CONN_LOG(LEVEL, FORMAT, CONNECTION, ...) \ - FINE_GRAIN_LOG(LEVEL, "[C{}] " FORMAT, (CONNECTION).id(), ##__VA_ARGS__) + FINE_GRAIN_GROUP_LOG(LEVEL, "", "[C{}] " FORMAT, (CONNECTION).id(), ##__VA_ARGS__) /** * Convenient macro for stream log. */ #define FINE_GRAIN_STREAM_LOG(LEVEL, FORMAT, STREAM, ...) \ - FINE_GRAIN_LOG(LEVEL, "[C{}][S{}] " FORMAT, \ - (STREAM).connection() ? (STREAM).connection()->id() : 0, (STREAM).streamId(), \ - ##__VA_ARGS__) + FINE_GRAIN_GROUP_LOG(LEVEL, "", "[C{}][S{}] " FORMAT, \ + (STREAM).connection() ? (STREAM).connection()->id() : 0, \ + (STREAM).streamId(), ##__VA_ARGS__) /** * Convenient macro for log flush. */ -#define FINE_GRAIN_FLUSH_LOG() \ +#define FINE_GRAIN_FLUSH_LOG(NAME) \ do { \ - SpdLoggerSharedPtr p = ::Envoy::getFineGrainLogContext().getFineGrainLogEntry(__FILE__); \ + SpdLoggerSharedPtr p = \ + ::Envoy::getFineGrainLogContext().getFineGrainLogEntryForFlush(__FILE__, NAME); \ if (p) { \ p->flush(); \ } \ diff --git a/source/common/common/inline_map.h b/source/common/common/inline_map.h index 4992a5971bf6d..e3372c94457bd 100644 --- a/source/common/common/inline_map.h +++ b/source/common/common/inline_map.h @@ -107,17 +107,17 @@ template class InlineMapDescriptor { /** * Fetch the handle for the given key. Return handle if the given key is inline key and return - * absl::nullopt if the given key is normal key. May only be called after finalize(). This should + * std::nullopt if the given key is normal key. May only be called after finalize(). This should * be used to get the handle of the inline keys that added by addHandle(). This function could * used to determine if a key is added as inline key or not at runtime or xDS config loading time * and decide if the key should be used as inline key or normal key. * Heterogeneous lookup is supported here. */ - template absl::optional getHandleByKey(const GetKey& key) const { + template std::optional getHandleByKey(const GetKey& key) const { ASSERT(finalized_, "Cannot get inline handle before finalize()"); if (auto it = inline_keys_map_.find(key); it == inline_keys_map_.end()) { - return absl::nullopt; + return std::nullopt; } else { return it->second; } @@ -212,7 +212,7 @@ template class InlineMap : public InlineStorage { return it->second; } - if (absl::optional entry_id = inlineLookup(key, hash); entry_id.has_value()) { + if (std::optional entry_id = inlineLookup(key, hash); entry_id.has_value()) { if (inlineEntryValid(*entry_id)) { return inline_entries_[*entry_id]; } @@ -236,7 +236,7 @@ template class InlineMap : public InlineStorage { return it->second; } - if (absl::optional entry_id = inlineLookup(key, hash); entry_id.has_value()) { + if (std::optional entry_id = inlineLookup(key, hash); entry_id.has_value()) { if (inlineEntryValid(*entry_id)) { return inline_entries_[*entry_id]; } @@ -281,7 +281,7 @@ template class InlineMap : public InlineStorage { */ template std::pair set(SetKey&& key, SetValue&& value) { - if (absl::optional entry_id = inlineLookup(key); entry_id.has_value()) { + if (std::optional entry_id = inlineLookup(key); entry_id.has_value()) { // This key is registered as inline key and try to insert the value to the inline array. // If the entry is already valid, insert will fail and return the ref of exist value. @@ -324,7 +324,7 @@ template class InlineMap : public InlineStorage { * @return the number of elements erased. */ template uint64_t erase(const GetKey& key) { - if (absl::optional entry_id = inlineLookup(key); entry_id.has_value()) { + if (std::optional entry_id = inlineLookup(key); entry_id.has_value()) { return clearInlineMapEntry(*entry_id); } else { return dynamic_entries_.erase(key); @@ -392,7 +392,7 @@ template class InlineMap : public InlineStorage { * Override operator [] to support the dynamic key assignment. */ template Value& operator[](const GetKey& key) { - if (absl::optional entry_id = inlineLookup(key); entry_id.has_value()) { + if (std::optional entry_id = inlineLookup(key); entry_id.has_value()) { // This key is registered as inline key and try to add the value to the inline array. if (!inlineEntryValid(*entry_id)) { resetInlineMapEntry(*entry_id, Value()); @@ -465,21 +465,21 @@ template class InlineMap : public InlineStorage { return 0; } - template absl::optional inlineLookup(const GetKey& key) const { + template std::optional inlineLookup(const GetKey& key) const { const auto& map_ref = descriptor_.inlineKeysMap(); if (auto iter = map_ref.find(key); iter != map_ref.end()) { return iter->second.inlineId(); } - return absl::nullopt; + return std::nullopt; } template - absl::optional inlineLookup(const GetKey& key, size_t hash) const { + std::optional inlineLookup(const GetKey& key, size_t hash) const { const auto& map_ref = descriptor_.inlineKeysMap(); if (auto iter = map_ref.find(key, hash); iter != map_ref.end()) { return iter->second.inlineId(); } - return absl::nullopt; + return std::nullopt; } bool inlineEntryValid(uint64_t inline_entry_id) const { diff --git a/source/common/common/key_value_store_base.cc b/source/common/common/key_value_store_base.cc index 69fce02c5ea66..4a5734fb567a2 100644 --- a/source/common/common/key_value_store_base.cc +++ b/source/common/common/key_value_store_base.cc @@ -9,8 +9,8 @@ namespace Envoy { namespace { // Removes a length prefixed token from |contents| and returns the token, -// or returns absl::nullopt on failure. -absl::optional getToken(absl::string_view& contents, std::string& error) { +// or returns std::nullopt on failure. +std::optional getToken(absl::string_view& contents, std::string& error) { const auto it = contents.find('\n'); if (it == contents.npos) { error = "Bad file: no newline"; @@ -58,9 +58,9 @@ KeyValueStoreBase::KeyValueStoreBase(Event::Dispatcher& dispatcher, bool KeyValueStoreBase::parseContents(absl::string_view contents) { std::string error; while (!contents.empty()) { - absl::optional key = getToken(contents, error); - absl::optional value; - absl::optional ttl = absl::nullopt; + std::optional key = getToken(contents, error); + std::optional value; + std::optional ttl = std::nullopt; if (key.has_value()) { value = getToken(contents, error); } @@ -92,7 +92,7 @@ bool KeyValueStoreBase::parseContents(absl::string_view contents) { } void KeyValueStoreBase::addOrUpdate(absl::string_view key_view, absl::string_view value_view, - absl::optional ttl) { + std::optional ttl) { ENVOY_BUG(!under_iterate_, "addOrUpdate under the stack of iterate"); std::string key(key_view); std::string value(value_view); @@ -101,7 +101,7 @@ void KeyValueStoreBase::addOrUpdate(absl::string_view key_view, absl::string_vie ASSERT(false); return; } - absl::optional absolute_ttl = absl::nullopt; + std::optional absolute_ttl = std::nullopt; if (ttl) { absolute_ttl.emplace(ttl.value() + std::chrono::duration_cast( time_source_.systemTime().time_since_epoch())); @@ -147,7 +147,7 @@ void KeyValueStoreBase::remove(absl::string_view key) { } } -absl::optional KeyValueStoreBase::get(absl::string_view key) { +std::optional KeyValueStoreBase::get(absl::string_view key) { auto it = store_.find(std::string(key)); if (it == store_.end()) { return {}; diff --git a/source/common/common/key_value_store_base.h b/source/common/common/key_value_store_base.h index 833435bff4d9e..05e1101e5bf59 100644 --- a/source/common/common/key_value_store_base.h +++ b/source/common/common/key_value_store_base.h @@ -33,18 +33,18 @@ class KeyValueStoreBase : public KeyValueStore, // KeyValueStore void addOrUpdate(absl::string_view key, absl::string_view value, - absl::optional ttl) override; + std::optional ttl) override; void remove(absl::string_view key) override; - absl::optional get(absl::string_view key) override; + std::optional get(absl::string_view key) override; void iterate(ConstIterateCb cb) const override; protected: // Values in a KeyValueStore have an optional TTL. struct ValueWithTtl { - ValueWithTtl(std::string&& value, absl::optional&& ttl) + ValueWithTtl(std::string&& value, std::optional&& ttl) : value_(std::move(value)), ttl_(std::move(ttl)) {} std::string value_; - absl::optional ttl_; + std::optional ttl_; }; using KeyValueMap = absl::linked_hash_map; diff --git a/source/common/common/logger.cc b/source/common/common/logger.cc index c2979d1072125..470988967c330 100644 --- a/source/common/common/logger.cc +++ b/source/common/common/logger.cc @@ -9,6 +9,7 @@ #include "source/common/common/json_escape_string.h" #include "source/common/common/lock_guard.h" +#include "source/common/version/version_string.h" #include "absl/strings/ascii.h" #include "absl/strings/escaping.h" @@ -174,7 +175,7 @@ void Context::changeAllLogLevels(spdlog::level::level_enum level) { } else { // Level setting with Fine-Grain Logger. FINE_GRAIN_LOG( - info, + info, "", "change all log levels and default verbosity level for fine grain loggers: level='{}'", spdlog::level::level_string_views[level]); getFineGrainLogContext().updateVerbosityDefaultLevel(level); @@ -321,6 +322,10 @@ void setLogFormatForLogger(spdlog::logger& logger, const std::string& log_format CustomFlagFormatter::ExtractedMessage::Placeholder) .set_pattern(log_format); + formatter + ->add_flag(CustomFlagFormatter::EnvoyVersion::Placeholder) + .set_pattern(log_format); + logger.set_formatter(std::move(formatter)); } @@ -420,6 +425,12 @@ void ExtractedMessage::format(const spdlog::details::log_msg& msg, const std::tm Envoy::Logger::Utility::escapeMessageJsonString(original_message, dest); } +void EnvoyVersion::format(const spdlog::details::log_msg&, const std::tm&, + spdlog::memory_buf_t& dest) { + const std::string& version = envoyVersionString(); + dest.append(version.data(), version.data() + version.size()); +} + } // namespace CustomFlagFormatter } // namespace Logger } // namespace Envoy diff --git a/source/common/common/logger.h b/source/common/common/logger.h index cd298c4659ecc..36bfdcf1ca647 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -104,7 +104,8 @@ const static bool should_log = true; FUNCTION(websocket) \ FUNCTION(golang) \ FUNCTION(stats_sinks) \ - FUNCTION(dynamic_modules) + FUNCTION(dynamic_modules) \ + FUNCTION(ip_tagging) // clang-format off enum class Id { @@ -480,6 +481,24 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { constexpr static char Placeholder = '+'; }; +/** + * When added to a formatter, this adds 'N' as a user defined flag in the log pattern that emits + * the Envoy version string set via setVersion(). Use %N in --log-format to include the running + * version in every log line. Call setVersion() once at server start-up (before any logs are + * written) with the result of VersionInfo::version(). + */ +class EnvoyVersion : public spdlog::custom_flag_formatter { +public: + void format(const spdlog::details::log_msg& msg, const std::tm& tm, + spdlog::memory_buf_t& dest) override; + + std::unique_ptr clone() const override { + return spdlog::details::make_unique(); + } + + constexpr static char Placeholder = 'N'; +}; + } // namespace CustomFlagFormatter } // namespace Logger @@ -498,7 +517,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { */ #define ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL) \ (Envoy::Logger::Context::useFineGrainLogger() \ - ? (ENVOY_SPDLOG_LEVEL(LEVEL) >= (*FINE_GRAIN_LOGGER()).level()) \ + ? (ENVOY_SPDLOG_LEVEL(LEVEL) >= (*FINE_GRAIN_LOGGER(LOGGER.name())).level()) \ : (ENVOY_SPDLOG_LEVEL(LEVEL) >= (LOGGER).level())) /** @@ -514,7 +533,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { } \ } while (0) -#define ENVOY_LOG_CHECK_LEVEL(LEVEL) ENVOY_LOG_COMP_LEVEL(ENVOY_LOGGER(), LEVEL) +#define ENVOY_LOG_CHECK_LEVEL(LEVEL) ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(ENVOY_LOGGER(), LEVEL) /** * Convenience macro to log to a user-specified logger. When fine-grain logging is used, the @@ -523,7 +542,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, ...) \ do { \ if (Envoy::Logger::should_log && Envoy::Logger::Context::useFineGrainLogger()) { \ - FINE_GRAIN_LOG(LEVEL, ##__VA_ARGS__); \ + FINE_GRAIN_GROUP_LOG(LEVEL, LOGGER.name(), ##__VA_ARGS__); \ } else { \ ENVOY_LOG_COMP_AND_LOG(LOGGER, LEVEL, ##__VA_ARGS__); \ } \ @@ -550,7 +569,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_TAGGED_LOG_TO_LOGGER(LOGGER, LEVEL, TAGS, FORMAT, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, "{}" FORMAT, \ ::Envoy::Logger::Utility::serializeLogTags(TAGS), ##__VA_ARGS__); \ } \ @@ -558,7 +577,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_TAGGED_CONN_LOG_TO_LOGGER(LOGGER, LEVEL, TAGS, CONNECTION, FORMAT, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ std::map log_tags = TAGS; \ log_tags.emplace("ConnectionId", std::to_string((CONNECTION).id())); \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, "{}" FORMAT, \ @@ -568,7 +587,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_TAGGED_STREAM_LOG_TO_LOGGER(LOGGER, LEVEL, TAGS, STREAM, FORMAT, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ std::map log_tags = TAGS; \ log_tags.emplace("ConnectionId", \ (STREAM).connection() ? std::to_string((STREAM).connection()->id()) : "0"); \ @@ -668,7 +687,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_LOG_FIRST_N_TO_LOGGER(LOGGER, LEVEL, N, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ static auto* countdown = new std::atomic(); \ if (countdown->fetch_add(1) < N) { \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, ##__VA_ARGS__); \ @@ -678,7 +697,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_LOG_FIRST_N_TO_LOGGER_IF(LOGGER, LEVEL, N, CONDITION, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL) && (CONDITION)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL) && (CONDITION)) { \ static auto* countdown = new std::atomic(); \ if (countdown->fetch_add(1) < N) { \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, ##__VA_ARGS__); \ @@ -717,7 +736,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_LOG_EVERY_NTH_TO_LOGGER(LOGGER, LEVEL, N, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ static auto* count = new std::atomic(); \ if ((count->fetch_add(1) % N) == 0) { \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, ##__VA_ARGS__); \ @@ -733,7 +752,7 @@ class ExtractedMessage : public spdlog::custom_flag_formatter { #define ENVOY_LOG_EVERY_POW_2_TO_LOGGER(LOGGER, LEVEL, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ static auto* count = new std::atomic(); \ if (std::bitset<64>(1 /* for the first hit*/ + count->fetch_add(1)).count() == 1) { \ ENVOY_LOG_TO_LOGGER(LOGGER, LEVEL, ##__VA_ARGS__); \ @@ -754,7 +773,7 @@ using t_logclock = std::chrono::steady_clock; // NOLINT #define ENVOY_LOG_PERIODIC_TO_LOGGER(LOGGER, LEVEL, CHRONO_DURATION, ...) \ do { \ - if (ENVOY_LOG_COMP_LEVEL(LOGGER, LEVEL)) { \ + if (ENVOY_LOG_COMP_LEVEL_FINE_GRAIN_IF(LOGGER, LEVEL)) { \ static auto* last_hit = new std::atomic(); \ auto last = last_hit->load(); \ const auto now = t_logclock::now().time_since_epoch().count(); \ @@ -775,7 +794,7 @@ using t_logclock = std::chrono::steady_clock; // NOLINT #define ENVOY_FLUSH_LOG() \ do { \ if (Envoy::Logger::Context::useFineGrainLogger()) { \ - FINE_GRAIN_FLUSH_LOG(); \ + FINE_GRAIN_FLUSH_LOG(ENVOY_LOGGER().name()); \ } else { \ ENVOY_LOGGER().flush(); \ } \ diff --git a/source/common/common/matchers.h b/source/common/common/matchers.h index fd8767eb318ce..5e4370d0d5d1a 100644 --- a/source/common/common/matchers.h +++ b/source/common/common/matchers.h @@ -241,7 +241,7 @@ class StringMatcherImpl : public ValueMatcher, public StringMatcher { } // StringMatcher - bool match(absl::string_view value) const override { return doMatch(value, absl::nullopt); } + bool match(absl::string_view value) const override { return doMatch(value, std::nullopt); } bool match(absl::string_view value, const StringMatcher::Context& context) const override { return doMatch(value, makeOptRef(context)); } diff --git a/source/common/common/posix/thread_impl.cc b/source/common/common/posix/thread_impl.cc index 6e53869e6f29f..103186f2e7bbb 100644 --- a/source/common/common/posix/thread_impl.cc +++ b/source/common/common/posix/thread_impl.cc @@ -8,6 +8,7 @@ #include "absl/strings/str_cat.h" #if defined(__linux__) +#include #include #include #elif defined(__APPLE__) @@ -62,6 +63,25 @@ void setThreadPriority(const int64_t tid, const int priority) { #endif } +void setThreadAffinity(const uint32_t cpu) { +#if defined(__linux__) + if (cpu >= CPU_SETSIZE) { + ENVOY_LOG_MISC(warn, "thread affinity CPU {} exceeds the supported maximum", cpu); + return; + } + cpu_set_t set; + CPU_ZERO(&set); + CPU_SET(cpu, &set); + const int rc = sched_setaffinity(0, sizeof(set), &set); + if (rc != 0) { + ENVOY_LOG_MISC(warn, "failed to set thread affinity to CPU {}: {}", cpu, + Envoy::errorDetails(errno)); + } +#else + UNREFERENCED_PARAMETER(cpu); +#endif +} + } // namespace // See https://www.man7.org/linux/man-pages/man3/pthread_setname_np.3.html. @@ -69,14 +89,17 @@ void setThreadPriority(const int64_t tid, const int priority) { // so we need to truncate the string_view to 15 bytes. #define PTHREAD_MAX_THREADNAME_LEN_INCLUDING_NULL_BYTE 16 -ThreadHandle::ThreadHandle(std::function thread_routine, - absl::optional thread_priority) - : thread_routine_(thread_routine), thread_priority_(thread_priority) {} +ThreadHandle::ThreadHandle(std::function thread_routine, std::optional thread_priority, + std::optional thread_cpu_affinity) + : thread_routine_(thread_routine), thread_priority_(thread_priority), + thread_cpu_affinity_(thread_cpu_affinity) {} /** Returns the thread routine. */ std::function& ThreadHandle::routine() { return thread_routine_; } -absl::optional ThreadHandle::priority() const { return thread_priority_; } +std::optional ThreadHandle::priority() const { return thread_priority_; } + +std::optional ThreadHandle::cpuAffinity() const { return thread_cpu_affinity_; } /** Returns the thread handle. */ pthread_t& ThreadHandle::handle() { return thread_handle_; } @@ -175,6 +198,9 @@ int PosixThreadFactory::createPthread(ThreadHandle* thread_handle) { if (handle->priority()) { setThreadPriority(getCurrentThreadId(), *handle->priority()); } + if (handle->cpuAffinity()) { + setThreadAffinity(*handle->cpuAffinity()); + } handle->routine()(); return nullptr; }, @@ -183,8 +209,8 @@ int PosixThreadFactory::createPthread(ThreadHandle* thread_handle) { PosixThreadPtr PosixThreadFactory::createThread(std::function thread_routine, OptionsOptConstRef options, bool crash_on_failure) { - auto thread_handle = - new ThreadHandle(thread_routine, options ? options->priority_ : absl::nullopt); + auto thread_handle = new ThreadHandle(thread_routine, options ? options->priority_ : std::nullopt, + options ? options->cpu_affinity_ : std::nullopt); const int rc = createPthread(thread_handle); if (rc != 0) { delete thread_handle; diff --git a/source/common/common/posix/thread_impl.h b/source/common/common/posix/thread_impl.h index dbfadb96aaf51..f98348dee4d77 100644 --- a/source/common/common/posix/thread_impl.h +++ b/source/common/common/posix/thread_impl.h @@ -12,20 +12,25 @@ namespace Thread { class ThreadHandle { public: - ThreadHandle(std::function thread_routine, absl::optional thread_priority); + ThreadHandle(std::function thread_routine, std::optional thread_priority, + std::optional thread_cpu_affinity); /** Returns the thread routine. */ std::function& routine(); /** Returns the thread priority, if any. */ - absl::optional priority() const; + std::optional priority() const; + + /** Returns the CPU to pin the thread to, if any. */ + std::optional cpuAffinity() const; /** Returns the thread handle. */ pthread_t& handle(); private: std::function thread_routine_; - const absl::optional thread_priority_; + const std::optional thread_priority_; + const std::optional thread_cpu_affinity_; pthread_t thread_handle_; }; diff --git a/source/common/common/radix_tree.h b/source/common/common/radix_tree.h index a7655f8503d29..bb923ecfe0c2b 100644 --- a/source/common/common/radix_tree.h +++ b/source/common/common/radix_tree.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/optref.h" @@ -10,7 +11,6 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/inlined_vector.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { diff --git a/source/common/common/scope_tracked_object_stack.h b/source/common/common/scope_tracked_object_stack.h index 880ddd8eeb3a0..0fcf1e4144b53 100644 --- a/source/common/common/scope_tracked_object_stack.h +++ b/source/common/common/scope_tracked_object_stack.h @@ -24,6 +24,7 @@ class ScopeTrackedObjectStack : public ScopeTrackedObject { void add(const ScopeTrackedObject& object) { tracked_objects_.push_back(object); } OptRef trackedStream() const override { + // NOLINTNEXTLINE(modernize-loop-convert) for (auto iter = tracked_objects_.rbegin(); iter != tracked_objects_.rend(); ++iter) { OptRef stream = iter->get().trackedStream(); if (stream.has_value()) { @@ -34,6 +35,7 @@ class ScopeTrackedObjectStack : public ScopeTrackedObject { } void dumpState(std::ostream& os, int indent_level) const override { + // NOLINTNEXTLINE(modernize-loop-convert) for (auto iter = tracked_objects_.rbegin(); iter != tracked_objects_.rend(); ++iter) { iter->get().dumpState(os, indent_level); } diff --git a/source/common/common/thread.cc b/source/common/common/thread.cc index a22c1754439f8..9c90438e98cd1 100644 --- a/source/common/common/thread.cc +++ b/source/common/common/thread.cc @@ -67,7 +67,7 @@ struct ThreadIds { private: mutable absl::Mutex mutex_; - std::atomic skip_asserts_{}; + std::atomic skip_asserts_{0}; absl::flat_hash_map main_threads_to_usage_count_ ABSL_GUARDED_BY(mutex_); }; diff --git a/source/common/common/utility.cc b/source/common/common/utility.cc index edee2cd36a5f8..276e3e1e222da 100644 --- a/source/common/common/utility.cc +++ b/source/common/common/utility.cc @@ -361,6 +361,16 @@ const char* StringUtil::strtoull(const char* str, uint64_t& out, int base) { return nullptr; } + // Reject signed values for unsigned parsing to avoid normalizing malformed + // inputs (e.g. "-1") into large uint64_t values. + const char* first_non_whitespace = str; + while (*first_non_whitespace != '\0' && absl::ascii_isspace(*first_non_whitespace)) { + ++first_non_whitespace; + } + if (*first_non_whitespace == '+' || *first_non_whitespace == '-') { + return nullptr; + } + char* end_ptr; errno = 0; out = std::strtoull(str, &end_ptr, base); diff --git a/source/common/common/utility.h b/source/common/common/utility.h index b75862d593a29..eb2d7c254b71b 100644 --- a/source/common/common/utility.h +++ b/source/common/common/utility.h @@ -313,6 +313,7 @@ class StringUtil { /** * Convert a string to an unsigned long, checking for error. + * Rejects leading '+' or '-' after optional ASCII whitespace. * @return pointer to the remainder of 'str' if successful, nullptr otherwise. */ static const char* strtoull(const char* str, uint64_t& out, int base = 10); diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 96ac32798366f..ba2ee65f1f9bf 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -234,7 +234,6 @@ envoy_cc_library( "//source/common/runtime:runtime_features_lib", "//source/common/singleton:const_singleton", "//source/common/version:api_version_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", @@ -245,8 +244,8 @@ envoy_cc_library( ) envoy_cc_library( - name = "subscription_base_interface", - hdrs = ["subscription_base.h"], + name = "resource_type_helper_lib", + hdrs = ["resource_type_helper.h"], deps = [ ":opaque_resource_decoder_lib", ":resource_name_lib", diff --git a/source/common/config/config_provider_impl.h b/source/common/config/config_provider_impl.h index 35709630d2fbc..3dd5c3a318bb8 100644 --- a/source/common/config/config_provider_impl.h +++ b/source/common/config/config_provider_impl.h @@ -149,7 +149,7 @@ class ConfigSubscriptionCommonBase : protected Logger::Loggable; struct LastConfigInfo { - absl::optional last_config_hash_; + std::optional last_config_hash_; std::string last_config_version_; }; @@ -164,7 +164,7 @@ class ConfigSubscriptionCommonBase : protected Logger::Loggable& configInfo() const { return config_info_; } + const std::optional& configInfo() const { return config_info_; } ConfigProvider::ConfigConstSharedPtr getConfig() { return tls_->config_; } @@ -209,12 +209,12 @@ class ConfigSubscriptionCommonBase : protected Logger::Loggable&& config_info) { + void setLastConfigInfo(std::optional&& config_info) { config_info_ = std::move(config_info); } const std::string name_; - absl::optional config_info_; + std::optional config_info_; // This slot holds a Config implementation in each thread, which is intended to be shared between // config providers from the same config source. ThreadLocal::TypedSlot tls_; diff --git a/source/common/config/datasource.cc b/source/common/config/datasource.cc index fa3fc9240f609..3d5813701e798 100644 --- a/source/common/config/datasource.cc +++ b/source/common/config/datasource.cc @@ -81,10 +81,10 @@ absl::StatusOr read(const envoy::config::core::v3::DataSource& sour return data; } -absl::optional getPath(const envoy::config::core::v3::DataSource& source) { +std::optional getPath(const envoy::config::core::v3::DataSource& source) { return source.specifier_case() == envoy::config::core::v3::DataSource::SpecifierCase::kFilename - ? absl::make_optional(source.filename()) - : absl::nullopt; + ? std::make_optional(source.filename()) + : std::nullopt; } bool usesFileWatching(const ProtoDataSource& source, const ProviderOptions& options) { diff --git a/source/common/config/datasource.h b/source/common/config/datasource.h index 05c8218cb6e01..e0de3818b7a07 100644 --- a/source/common/config/datasource.h +++ b/source/common/config/datasource.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/api/api.h" #include "envoy/common/random_generator.h" @@ -19,8 +20,6 @@ #include "source/common/config/watched_directory.h" #include "source/common/init/target_impl.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Config { namespace DataSource { @@ -62,9 +61,9 @@ absl::StatusOr read(const envoy::config::core::v3::DataSource& sour /** * @param source data source. - * @return absl::optional path to DataSource if a filename, otherwise absl::nullopt. + * @return std::optional path to DataSource if a filename, otherwise std::nullopt. */ -absl::optional getPath(const envoy::config::core::v3::DataSource& source); +std::optional getPath(const envoy::config::core::v3::DataSource& source); template using DataTransform = std::function>(absl::string_view)>; @@ -95,7 +94,7 @@ template class DynamicData { std::shared_ptr initial_data, uint64_t initial_hash, absl::AnyInvocable cleanup, absl::Status& creation_status) : dispatcher_(main_dispatcher), api_(api), options_(options), filename_(source.filename()), - data_transform_(data_transform_cb), hash_(initial_hash), cleanup_(std::move(cleanup)) { + data_transform_cb_(data_transform_cb), hash_(initial_hash), cleanup_(std::move(cleanup)) { slot_ = ThreadLocal::TypedSlot::ThreadLocalData>::makeUnique(tls); slot_->set([initial_data = std::move(initial_data)](Event::Dispatcher&) { @@ -148,7 +147,7 @@ template class DynamicData { return absl::OkStatus(); } } - auto transformed_new_data_or_error = data_transform_(new_data_or_error.value()); + auto transformed_new_data_or_error = data_transform_cb_(new_data_or_error.value()); if (!transformed_new_data_or_error.ok()) { // Log an error but don't fail the watch to avoid throwing EnvoyException at runtime. ENVOY_LOG_TO_LOGGER(Logger::Registry::getLog(Logger::Id::config), error, @@ -173,7 +172,7 @@ template class DynamicData { Api::Api& api_; const ProviderOptions options_; const std::string filename_; - DataTransform data_transform_; + DataTransform data_transform_cb_; uint64_t hash_; absl::AnyInvocable cleanup_; ThreadLocal::TypedSlotPtr slot_; @@ -214,7 +213,7 @@ template class DataSourceProvider { ThreadLocal::SlotAllocator& tls, Api::Api& api, bool allow_empty, DataTransform data_transform_cb, uint64_t max_size) { return create(source, main_dispatcher, tls, api, data_transform_cb, - {.allow_empty = allow_empty, .max_size = max_size}); + {.allow_empty = allow_empty, .max_size = max_size}, {}); } static absl::StatusOr> @@ -292,14 +291,14 @@ class ProviderSingleton : public Singleton::Instance, ProviderSingleton(Event::Dispatcher& main_dispatcher, ThreadLocal::SlotAllocator& tls, Api::Api& api, DataTransform data_transform_cb, const ProviderOptions& options) - : dispatcher_(main_dispatcher), tls_(tls), api_(api), data_transform_(data_transform_cb), + : dispatcher_(main_dispatcher), tls_(tls), api_(api), data_transform_cb_(data_transform_cb), options_(options) {} absl::StatusOr> getOrCreate(const ProtoDataSource& source) { ASSERT_IS_MAIN_OR_TEST_THREAD(); if (!usesFileWatching(source, options_)) { - return DataSourceProvider::create(source, dispatcher_, tls_, api_, data_transform_, - options_); + return DataSourceProvider::create(source, dispatcher_, tls_, api_, + data_transform_cb_, options_, {}); } const size_t config_hash = MessageUtil::hash(source); auto it = dynamic_providers_.find(config_hash); @@ -313,7 +312,7 @@ class ProviderSingleton : public Singleton::Instance, // Cleanup is guaranteed to execute on the main dispatcher during destruction but may happen // after the singleton is released. auto provider_or_error = DataSourceProvider::create( - source, dispatcher_, tls_, api_, data_transform_, options_, + source, dispatcher_, tls_, api_, data_transform_cb_, options_, [weak_this = this->weak_from_this(), config_hash] { if (auto locked_this = weak_this.lock(); locked_this) { locked_this->cleanup(config_hash); @@ -335,7 +334,7 @@ class ProviderSingleton : public Singleton::Instance, Event::Dispatcher& dispatcher_; ThreadLocal::SlotAllocator& tls_; Api::Api& api_; - DataTransform data_transform_; + DataTransform data_transform_cb_; const ProviderOptions options_; absl::flat_hash_map>> dynamic_providers_; }; diff --git a/source/common/config/decoded_resource_impl.h b/source/common/config/decoded_resource_impl.h index b268d3e387d62..4e720a2e47fb5 100644 --- a/source/common/config/decoded_resource_impl.h +++ b/source/common/config/decoded_resource_impl.h @@ -38,8 +38,8 @@ class DecodedResourceImpl : public DecodedResource { } return std::unique_ptr(new DecodedResourceImpl( - resource_decoder, absl::nullopt, Protobuf::RepeatedPtrField(), resource, true, - version, absl::nullopt, absl::nullopt)); + resource_decoder, std::nullopt, Protobuf::RepeatedPtrField(), resource, true, + version, std::nullopt, std::nullopt)); } static DecodedResourceImplPtr @@ -53,19 +53,19 @@ class DecodedResourceImpl : public DecodedResource { : DecodedResourceImpl( resource_decoder, resource.name(), resource.aliases(), resource.resource(), resource.has_resource(), resource.version(), - resource.has_ttl() ? absl::make_optional(std::chrono::milliseconds( + resource.has_ttl() ? std::make_optional(std::chrono::milliseconds( DurationUtil::durationToMilliseconds(resource.ttl()))) - : absl::nullopt, - resource.has_metadata() ? absl::make_optional(resource.metadata()) : absl::nullopt) {} + : std::nullopt, + resource.has_metadata() ? std::make_optional(resource.metadata()) : std::nullopt) {} DecodedResourceImpl(OpaqueResourceDecoder& resource_decoder, const xds::core::v3::CollectionEntry::InlineEntry& inline_entry) : DecodedResourceImpl(resource_decoder, inline_entry.name(), Protobuf::RepeatedPtrField(), inline_entry.resource(), - true, inline_entry.version(), absl::nullopt, absl::nullopt) {} + true, inline_entry.version(), std::nullopt, std::nullopt) {} DecodedResourceImpl(ProtobufTypes::MessagePtr resource, const std::string& name, const std::vector& aliases, const std::string& version) : resource_(std::move(resource)), has_resource_(true), name_(name), aliases_(aliases), - version_(version), ttl_(absl::nullopt), metadata_(absl::nullopt) {} + version_(version), ttl_(std::nullopt), metadata_(std::nullopt) {} // Config::DecodedResource const std::string& name() const override { return name_; } @@ -73,17 +73,17 @@ class DecodedResourceImpl : public DecodedResource { const std::string& version() const override { return version_; }; const Protobuf::Message& resource() const override { return *resource_; }; bool hasResource() const override { return has_resource_; } - absl::optional ttl() const override { return ttl_; } + std::optional ttl() const override { return ttl_; } const OptRef metadata() const override { - return metadata_.has_value() ? makeOptRef(metadata_.value()) : absl::nullopt; + return metadata_.has_value() ? makeOptRef(metadata_.value()) : std::nullopt; } private: - DecodedResourceImpl(OpaqueResourceDecoder& resource_decoder, absl::optional name, + DecodedResourceImpl(OpaqueResourceDecoder& resource_decoder, std::optional name, const Protobuf::RepeatedPtrField& aliases, const Protobuf::Any& resource, bool has_resource, const std::string& version, - absl::optional ttl, - const absl::optional& metadata) + std::optional ttl, + const std::optional& metadata) : resource_(resource_decoder.decodeResource(resource)), has_resource_(has_resource), name_(name ? *name : resource_decoder.resourceName(*resource_)), aliases_(repeatedPtrFieldToVector(aliases)), version_(version), ttl_(ttl), @@ -95,11 +95,11 @@ class DecodedResourceImpl : public DecodedResource { const std::vector aliases_; const std::string version_; // Per resource TTL. - const absl::optional ttl_; + const std::optional ttl_; // This is the metadata info under the Resource wrapper. // It is intended to be consumed in the xds_config_tracker extension. - const absl::optional metadata_; + const std::optional metadata_; }; struct DecodedResourcesWrapper { diff --git a/source/common/config/null_grpc_mux_impl.h b/source/common/config/null_grpc_mux_impl.h index 9797edb644cfb..590d32f30bf8c 100644 --- a/source/common/config/null_grpc_mux_impl.h +++ b/source/common/config/null_grpc_mux_impl.h @@ -27,13 +27,14 @@ class NullGrpcMuxImpl : public GrpcMux, ENVOY_BUG(false, "unexpected request for on demand update"); } - absl::Status updateMuxSource(Grpc::RawAsyncClientSharedPtr&&, Grpc::RawAsyncClientSharedPtr&&, - Stats::Scope&, BackOffStrategyPtr&&, - const envoy::config::core::v3::ApiConfigSource&) override { + absl::Status updateMuxSource( + Grpc::RawAsyncClientSharedPtr&&, Grpc::RawAsyncClientSharedPtr&&, Stats::Scope&, + BackOffStrategyPtr&&, const envoy::config::core::v3::ApiConfigSource&, + std::function()> = nullptr) override { return absl::UnimplementedError(""); } - EdsResourcesCacheOptRef edsResourcesCache() override { return absl::nullopt; } + EdsResourcesCacheOptRef edsResourcesCache() override { return std::nullopt; } Upstream::LoadStatsReporter* loadStatsReporter() const override { return nullptr; } Upstream::LoadStatsReporter* maybeCreateLoadStatsReporter() override { return nullptr; } diff --git a/source/common/config/subscription_base.h b/source/common/config/resource_type_helper.h similarity index 50% rename from source/common/config/subscription_base.h rename to source/common/config/resource_type_helper.h index dc15941c67200..31b7f3edfe087 100644 --- a/source/common/config/subscription_base.h +++ b/source/common/config/resource_type_helper.h @@ -8,17 +8,23 @@ namespace Envoy { namespace Config { -template struct SubscriptionBase : public Config::SubscriptionCallbacks { +/** + * Helper for resource type decoding and name identification. + * This class is intended to be used via composition in xDS API implementations. + */ +template class ResourceTypeHelper { public: - SubscriptionBase(ProtobufMessage::ValidationVisitor& validation_visitor, - absl::string_view name_field) + ResourceTypeHelper(ProtobufMessage::ValidationVisitor& validation_visitor, + absl::string_view name_field) : resource_decoder_(std::make_shared>( validation_visitor, name_field)) {} std::string getResourceName() const { return Envoy::Config::getResourceName(); } -protected: - OpaqueResourceDecoderSharedPtr resource_decoder_; + OpaqueResourceDecoderSharedPtr resourceDecoder() const { return resource_decoder_; } + +private: + const OpaqueResourceDecoderSharedPtr resource_decoder_; }; } // namespace Config diff --git a/source/common/config/subscription_factory_impl.cc b/source/common/config/subscription_factory_impl.cc index 00c5bb9882937..f3755123e4f44 100644 --- a/source/common/config/subscription_factory_impl.cc +++ b/source/common/config/subscription_factory_impl.cc @@ -45,7 +45,7 @@ absl::StatusOr SubscriptionFactoryImpl::subscriptionFromConfigS callbacks, resource_decoder, options, - absl::nullopt, + std::nullopt, Utility::generateStats(scope), cm_.adsMux()}; @@ -145,7 +145,7 @@ absl::StatusOr SubscriptionFactoryImpl::subscriptionOverAdsGrpc callbacks, resource_decoder, options, - absl::nullopt, + std::nullopt, Utility::generateStats(scope), ads_grpc_mux}; static constexpr absl::string_view subscription_type = "envoy.config_subscription.ads"; diff --git a/source/common/config/ttl.cc b/source/common/config/ttl.cc index 86dd100c71fc0..36a8e295bf214 100644 --- a/source/common/config/ttl.cc +++ b/source/common/config/ttl.cc @@ -10,7 +10,7 @@ TtlManager::TtlManager(std::function&)> call ScopedTtlUpdate scoped_update(*this); std::vector expired; - last_scheduled_time_ = absl::nullopt; + last_scheduled_time_ = std::nullopt; const auto now = time_source_.monotonicTime(); auto itr = ttls_.begin(); diff --git a/source/common/config/ttl.h b/source/common/config/ttl.h index 3c679a350dc14..32d0041b8abfc 100644 --- a/source/common/config/ttl.h +++ b/source/common/config/ttl.h @@ -58,7 +58,7 @@ class TtlManager { absl::flat_hash_map ttl_lookup_; Event::TimerPtr timer_; - absl::optional last_scheduled_time_; + std::optional last_scheduled_time_; uint8_t scoped_update_counter_{}; std::function&)> callback_; diff --git a/source/common/config/utility.cc b/source/common/config/utility.cc index 5974604f0fd78..742eeaecbf992 100644 --- a/source/common/config/utility.cc +++ b/source/common/config/utility.cc @@ -1,5 +1,7 @@ #include "source/common/config/utility.h" +#include + #include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "envoy/config/cluster/v3/cluster.pb.h" #include "envoy/config/core/v3/address.pb.h" @@ -14,7 +16,6 @@ #include "source/common/protobuf/utility.h" #include "absl/status/status.h" -#include "absl/types/optional.h" namespace Envoy { namespace Config { @@ -177,7 +178,7 @@ absl::Status Utility::checkApiConfigSourceSubscriptionBackingCluster( return absl::OkStatus(); } -absl::optional +std::optional Utility::getGrpcControlPlane(const envoy::config::core::v3::ApiConfigSource& api_config_source) { if (api_config_source.grpc_services_size() > 0) { std::string res = ""; @@ -198,7 +199,7 @@ Utility::getGrpcControlPlane(const envoy::config::core::v3::ApiConfigSource& api } return res; } - return absl::nullopt; + return std::nullopt; } std::chrono::milliseconds Utility::configSourceInitialFetchTimeout( @@ -266,7 +267,7 @@ Utility::getGrpcConfigFromApiConfigSource( if (grpc_service_idx >= api_config_source.grpc_services_size()) { // No returned factory in case there's no entry. - return absl::nullopt; + return std::nullopt; } return Envoy::makeOptRef(api_config_source.grpc_services(grpc_service_idx)); @@ -350,11 +351,11 @@ absl::Status Utility::translateOpaqueConfig(const Protobuf::Any& typed_config, absl::StatusOr Utility::buildJitteredExponentialBackOffStrategy( - absl::optional backoff, + std::optional backoff, Random::RandomGenerator& random, const uint32_t default_base_interval_ms, - absl::optional default_max_interval_ms) { + std::optional default_max_interval_ms) { // BackoffStrategy config is specified - if (backoff != absl::nullopt) { + if (backoff != std::nullopt) { uint32_t base_interval_ms = PROTOBUF_GET_MS_REQUIRED(backoff.value(), base_interval); uint32_t max_interval_ms = PROTOBUF_GET_MS_OR_DEFAULT(backoff.value(), max_interval, base_interval_ms * 10); @@ -373,7 +374,7 @@ Utility::buildJitteredExponentialBackOffStrategy( } // default maximum interval is specified - if (default_max_interval_ms != absl::nullopt) { + if (default_max_interval_ms != std::nullopt) { if (default_max_interval_ms.value() < default_base_interval_ms) { return absl::InvalidArgumentError( "default_max_interval_ms must be greater than or equal to the default_base_interval_ms"); diff --git a/source/common/config/utility.h b/source/common/config/utility.h index 469ffd47e2264..46bb41eaf3ee3 100644 --- a/source/common/config/utility.h +++ b/source/common/config/utility.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "envoy/api/api.h" #include "envoy/common/random_generator.h" #include "envoy/config/bootstrap/v3/bootstrap.pb.h" @@ -26,7 +28,6 @@ #include "source/common/version/api_version.h" #include "source/common/version/api_version_struct.h" -#include "absl/types/optional.h" #include "udpa/type/v1/typed_struct.pb.h" #include "xds/type/v3/typed_struct.pb.h" @@ -142,9 +143,9 @@ class Utility { * Gets the gRPC control plane management server from the API config source. The result is either * a cluster name or a host name. * @param api_config_source the config source to validate. - * @return the gRPC control plane server, or absl::nullopt if it couldn't be extracted. + * @return the gRPC control plane server, or std::nullopt if it couldn't be extracted. */ - static absl::optional + static std::optional getGrpcControlPlane(const envoy::config::core::v3::ApiConfigSource& api_config_source); /** @@ -251,13 +252,7 @@ class Utility { */ template static Factory* getFactory(const ProtoMessage& message) { - Factory* factory = Utility::getFactoryByType(message.typed_config()); - if (factory != nullptr || - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.no_extension_lookup_by_name")) { - return factory; - } - - return Utility::getFactoryByName(message.name()); + return Utility::getFactoryByType(message.typed_config()); } /** @@ -270,18 +265,12 @@ class Utility { template static Factory* getAndCheckFactory(const ProtoMessage& message, bool is_optional) { Factory* factory = Utility::getFactoryByType(message.typed_config()); - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.no_extension_lookup_by_name")) { - if (factory == nullptr && !is_optional) { - ExceptionUtil::throwEnvoyException( - fmt::format("Didn't find a registered implementation for '{}' with type URL: '{}'", - message.name(), getFactoryType(message.typed_config()))); - } - return factory; - } else if (factory != nullptr) { - return factory; + if (factory == nullptr && !is_optional) { + ExceptionUtil::throwEnvoyException( + fmt::format("Didn't find a registered implementation for '{}' with type URL: '{}'", + message.name(), getFactoryType(message.typed_config()))); } - - return Utility::getAndCheckFactoryByName(message.name(), is_optional); + return factory; } /** @@ -489,13 +478,13 @@ class Utility { prepareJitteredExponentialBackOffStrategy( const envoy::config::core::v3::ApiConfigSource& api_config_source, Random::RandomGenerator& random, const uint32_t default_base_interval_ms, - absl::optional default_max_interval_ms) { + std::optional default_max_interval_ms) { auto& grpc_services = api_config_source.grpc_services(); if (!grpc_services.empty() && grpc_services[0].has_envoy_grpc()) { return prepareJitteredExponentialBackOffStrategy( grpc_services[0].envoy_grpc(), random, default_base_interval_ms, default_max_interval_ms); } - return buildJitteredExponentialBackOffStrategy(absl::nullopt, random, default_base_interval_ms, + return buildJitteredExponentialBackOffStrategy(std::nullopt, random, default_base_interval_ms, default_max_interval_ms); } @@ -511,16 +500,16 @@ class Utility { */ template static absl::StatusOr - prepareJitteredExponentialBackOffStrategy( - const T& config, Random::RandomGenerator& random, const uint32_t default_base_interval_ms, - absl::optional default_max_interval_ms) { + prepareJitteredExponentialBackOffStrategy(const T& config, Random::RandomGenerator& random, + const uint32_t default_base_interval_ms, + std::optional default_max_interval_ms) { // If RetryPolicy containing backoff values is found in config if (config.has_retry_policy() && config.retry_policy().has_retry_back_off()) { return buildJitteredExponentialBackOffStrategy(config.retry_policy().retry_back_off(), random, default_base_interval_ms, default_max_interval_ms); } - return buildJitteredExponentialBackOffStrategy(absl::nullopt, random, default_base_interval_ms, + return buildJitteredExponentialBackOffStrategy(std::nullopt, random, default_base_interval_ms, default_max_interval_ms); } @@ -538,9 +527,9 @@ class Utility { */ static absl::StatusOr buildJitteredExponentialBackOffStrategy( - absl::optional backoff, + std::optional backoff, Random::RandomGenerator& random, const uint32_t default_base_interval_ms, - absl::optional default_max_interval_ms); + std::optional default_max_interval_ms); }; } // namespace Config } // namespace Envoy diff --git a/source/common/config/watched_directory.cc b/source/common/config/watched_directory.cc index 8e034b155dcc9..159bd65e1c45d 100644 --- a/source/common/config/watched_directory.cc +++ b/source/common/config/watched_directory.cc @@ -16,8 +16,11 @@ WatchedDirectory::create(const envoy::config::core::v3::WatchedDirectory& config WatchedDirectory::WatchedDirectory(const envoy::config::core::v3::WatchedDirectory& config, Event::Dispatcher& dispatcher, absl::Status& creation_status) { watcher_ = dispatcher.createFilesystemWatcher(); - SET_AND_RETURN_IF_NOT_OK(watcher_->addWatch(absl::StrCat(config.path(), "/"), - Filesystem::Watcher::Events::MovedTo, + auto events = Filesystem::Watcher::Events::MovedTo; + if (config.watch_modify()) { + events = events | Filesystem::Watcher::Events::Modified; + } + SET_AND_RETURN_IF_NOT_OK(watcher_->addWatch(absl::StrCat(config.path(), "/"), events, [this](uint32_t) { // Check if callback is set before invoking to avoid // crash if watch triggers before setCallback(). diff --git a/source/common/config/xds_manager_impl.cc b/source/common/config/xds_manager_impl.cc index b9ec03b71a554..118baf2203ceb 100644 --- a/source/common/config/xds_manager_impl.cc +++ b/source/common/config/xds_manager_impl.cc @@ -232,9 +232,8 @@ XdsManagerImpl::initializeAdsConnections(const envoy::config::bootstrap::v3::Boo std::function()> lrs_factory = [&, primary_client]() -> std::unique_ptr { - auto reporter = std::make_unique( + return std::make_unique( local_info_, *cm_, *stats_.rootScope(), primary_client, main_thread_dispatcher_); - return reporter; }; ads_mux_ = factory->create(std::move(primary_client), std::move(failover_client), @@ -265,9 +264,8 @@ XdsManagerImpl::initializeAdsConnections(const envoy::config::bootstrap::v3::Boo std::function()> lrs_factory = [&, primary_client]() -> std::unique_ptr { - auto reporter = std::make_unique( + return std::make_unique( local_info_, *cm_, *stats_.rootScope(), primary_client, main_thread_dispatcher_); - return reporter; }; OptRef xds_resources_delegate = @@ -468,9 +466,8 @@ XdsManagerImpl::createAuthority(const envoy::config::core::v3::ConfigSource& con failover_client)); std::function()> lrs_factory = [&, primary_client]() -> std::unique_ptr { - auto reporter = std::make_unique( + return std::make_unique( local_info_, *cm_, *stats_.rootScope(), primary_client, main_thread_dispatcher_); - return reporter; }; authority_mux = factory->create( @@ -504,9 +501,8 @@ XdsManagerImpl::createAuthority(const envoy::config::core::v3::ConfigSource& con std::function()> lrs_factory = [&, primary_client]() -> std::unique_ptr { - auto reporter = std::make_unique( + return std::make_unique( local_info_, *cm_, *stats_.rootScope(), primary_client, main_thread_dispatcher_); - return reporter; }; authority_mux = factory->create( @@ -591,9 +587,16 @@ XdsManagerImpl::replaceAdsMux(const envoy::config::core::v3::ApiConfigSource& ad // The failover_client may be null (no failover defined). ASSERT(primary_client != nullptr); + std::function()> lrs_factory = + [&, primary_client]() -> std::unique_ptr { + return std::make_unique( + local_info_, *cm_, *stats_.rootScope(), primary_client, main_thread_dispatcher_); + }; + // This will cause a disconnect from the current sources, and replacement of the clients. status = ads_mux_->updateMuxSource(std::move(primary_client), std::move(failover_client), - *stats_.rootScope(), std::move(backoff_strategy), ads_config); + *stats_.rootScope(), std::move(backoff_strategy), ads_config, + lrs_factory); return status; } diff --git a/source/common/conn_pool/conn_pool_base.cc b/source/common/conn_pool/conn_pool_base.cc index 2c1aa0f6fd233..4738dbdb60f3f 100644 --- a/source/common/conn_pool/conn_pool_base.cc +++ b/source/common/conn_pool/conn_pool_base.cc @@ -59,7 +59,9 @@ ConnPoolImplBase::ConnPoolImplBase( transport_socket_options_(transport_socket_options), cluster_connectivity_state_(state), upstream_ready_cb_(dispatcher_.createSchedulableCallback([this]() { onUpstreamReady(); })), create_new_connection_load_shed_(overload_manager.getLoadShedPoint( - Server::LoadShedPointName::get().ConnectionPoolNewConnection)) { + Server::LoadShedPointName::get().ConnectionPoolNewConnection)), + skip_pending_overflow_on_active_rq_(Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.skip_pending_overflow_count_on_active_rq")) { ENVOY_LOG_ONCE_IF(trace, create_new_connection_load_shed_ == nullptr, "LoadShedPoint envoy.load_shed_points.connection_pool_new_connection is not " "found. Is it configured?"); @@ -241,7 +243,10 @@ void ConnPoolImplBase::attachStreamToClient(Envoy::ConnectionPool::ActiveClient& ENVOY_LOG(debug, "max streams overflow"); onPoolFailure(client.real_host_description_, absl::string_view(), ConnectionPool::PoolFailureReason::Overflow, context); - traffic_stats.upstream_rq_pending_overflow_.inc(); + traffic_stats.upstream_rq_active_overflow_.inc(); + if (!skip_pending_overflow_on_active_rq_) { + traffic_stats.upstream_rq_pending_overflow_.inc(); + } return; } ENVOY_CONN_LOG(debug, "creating stream", client); @@ -399,9 +404,15 @@ void ConnPoolImplBase::onUpstreamReady() { ActiveClientPtr& client = ready_clients_.front(); ENVOY_CONN_LOG(debug, "attaching to next stream", *client); // Pending streams are pushed onto the front, so pull from the back. - attachStreamToClient(*client, pending_streams_.back()->context()); - cluster_connectivity_state_.decrPendingStreams(1); - pending_streams_.pop_back(); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.conn_pool_fix_reentrancy")) { + PendingStreamPtr pending_stream = pending_streams_.back()->removeFromList(pending_streams_); + cluster_connectivity_state_.decrPendingStreams(1); + attachStreamToClient(*client, pending_stream->context()); + } else { + attachStreamToClient(*client, pending_streams_.back()->context()); + cluster_connectivity_state_.decrPendingStreams(1); + pending_streams_.pop_back(); + } } if (!pending_streams_.empty()) { tryCreateNewConnections(); @@ -657,7 +668,7 @@ void ConnPoolImplBase::onConnectionEvent(ActiveClient& client, absl::string_view } // Now that the active client is ready, set up a timer for max connection duration. - const absl::optional max_connection_duration = + const std::optional max_connection_duration = client.parent_.host()->cluster().maxConnectionDuration(); if (max_connection_duration.has_value()) { client.connection_duration_timer_ = client.parent_.dispatcher().createTimer( @@ -696,11 +707,13 @@ PendingStream::PendingStream(ConnPoolImplBase& parent, bool can_send_early_data) Upstream::ClusterTrafficStats& traffic_stats = *parent_.host()->cluster().trafficStats(); traffic_stats.upstream_rq_pending_total_.inc(); traffic_stats.upstream_rq_pending_active_.inc(); + parent_.host()->stats().rq_pending_active_.inc(); parent_.host()->cluster().resourceManager(parent_.priority()).pendingRequests().inc(); } PendingStream::~PendingStream() { parent_.host()->cluster().trafficStats()->upstream_rq_pending_active_.dec(); + parent_.host()->stats().rq_pending_active_.dec(); parent_.host()->cluster().resourceManager(parent_.priority()).pendingRequests().dec(); } @@ -817,9 +830,15 @@ void ConnPoolImplBase::onUpstreamReadyForEarlyData(ActiveClient& client) { if (stream.can_send_early_data_) { ENVOY_CONN_LOG(debug, "creating stream for early data.", client); - attachStreamToClient(client, stream.context()); - cluster_connectivity_state_.decrPendingStreams(1); - stream.removeFromList(pending_streams_); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.conn_pool_fix_reentrancy")) { + PendingStreamPtr pending_stream = stream.removeFromList(pending_streams_); + cluster_connectivity_state_.decrPendingStreams(1); + attachStreamToClient(client, pending_stream->context()); + } else { + attachStreamToClient(client, stream.context()); + cluster_connectivity_state_.decrPendingStreams(1); + stream.removeFromList(pending_streams_); + } } if (stop_iteration) { return; diff --git a/source/common/conn_pool/conn_pool_base.h b/source/common/conn_pool/conn_pool_base.h index 278d484ddf491..eebba114561e0 100644 --- a/source/common/conn_pool/conn_pool_base.h +++ b/source/common/conn_pool/conn_pool_base.h @@ -58,8 +58,8 @@ class ActiveClient : public LinkedObject, return std::min(remaining_streams_, concurrent_stream_limit_); } - // Returns the application protocol, or absl::nullopt for TCP. - virtual absl::optional protocol() const PURE; + // Returns the application protocol, or std::nullopt for TCP. + virtual std::optional protocol() const PURE; virtual int64_t currentUnusedCapacity() const { int64_t remaining_concurrent_streams = @@ -421,6 +421,9 @@ class ConnPoolImplBase : protected Logger::Loggable { Event::SchedulableCallbackPtr upstream_ready_cb_; Common::DebugRecursionChecker recursion_checker_; Server::LoadShedPoint* create_new_connection_load_shed_{nullptr}; + +protected: + bool skip_pending_overflow_on_active_rq_; }; } // namespace ConnectionPool diff --git a/source/common/event/dispatcher_impl.cc b/source/common/event/dispatcher_impl.cc index 6cc1e52312528..bedfe219d331d 100644 --- a/source/common/event/dispatcher_impl.cc +++ b/source/common/event/dispatcher_impl.cc @@ -99,7 +99,7 @@ void DispatcherImpl::registerWatchdog(const Server::WatchDogSharedPtr& watchdog, } void DispatcherImpl::initializeStats(Stats::Scope& scope, - const absl::optional& prefix) { + const std::optional& prefix) { const std::string effective_prefix = prefix.has_value() ? *prefix : absl::StrCat(name_, "."); // This needs to be run in the dispatcher's thread, so that we have a thread id to log. post([this, &scope, effective_prefix] { @@ -386,6 +386,7 @@ void DispatcherImpl::onFatalError(std::ostream& os) const { // Dump the state of the tracked objects in the dispatcher if thread safe. This generally // results in dumping the active state only for the thread which caused the fatal error. if (isThreadSafe()) { + // NOLINTNEXTLINE(modernize-loop-convert) for (auto iter = tracked_object_stack_.rbegin(); iter != tracked_object_stack_.rend(); ++iter) { (*iter)->dumpState(os); } diff --git a/source/common/event/dispatcher_impl.h b/source/common/event/dispatcher_impl.h index 5e48b2a838297..7c23f1a47bc02 100644 --- a/source/common/event/dispatcher_impl.h +++ b/source/common/event/dispatcher_impl.h @@ -60,7 +60,7 @@ class DispatcherImpl : Logger::Loggable, void registerWatchdog(const Server::WatchDogSharedPtr& watchdog, std::chrono::milliseconds min_touch_interval) override; TimeSource& timeSource() override { return time_source_; } - void initializeStats(Stats::Scope& scope, const absl::optional& prefix) override; + void initializeStats(Stats::Scope& scope, const std::optional& prefix) override; void clearDeferredDeleteList() override; Network::ServerConnectionPtr createServerConnection(Network::ConnectionSocketPtr&& socket, diff --git a/source/common/event/timer_impl.h b/source/common/event/timer_impl.h index d5612f37e20f6..314d536bdf470 100644 --- a/source/common/event/timer_impl.h +++ b/source/common/event/timer_impl.h @@ -71,7 +71,7 @@ class TimerImpl : public Timer, ImplBase { // This has to be atomic for alarms which are handled out of thread, for // example if the DispatcherImpl::post is called by two threads, they race to // both set this to null. - std::atomic object_{}; + std::atomic object_{nullptr}; }; } // namespace Event diff --git a/source/common/filesystem/posix/directory_iterator_impl.cc b/source/common/filesystem/posix/directory_iterator_impl.cc index 2b9167f464e24..09583af2aec2a 100644 --- a/source/common/filesystem/posix/directory_iterator_impl.cc +++ b/source/common/filesystem/posix/directory_iterator_impl.cc @@ -17,7 +17,7 @@ DirectoryIteratorImpl::DirectoryIteratorImpl(const std::string& directory_path) if (status_.ok()) { nextEntry(); } else { - entry_ = {"", FileType::Other, absl::nullopt}; + entry_ = {"", FileType::Other, std::nullopt}; } } @@ -46,7 +46,7 @@ void DirectoryIteratorImpl::nextEntry() { dirent* entry = ::readdir(dir_); if (entry == nullptr) { - entry_ = {"", FileType::Other, absl::nullopt}; + entry_ = {"", FileType::Other, std::nullopt}; if (errno != 0) { status_ = absl::ErrnoToStatus(errno, fmt::format("unable to iterate directory {}: {}", directory_path_, errorDetails(errno))); @@ -75,18 +75,18 @@ absl::StatusOr DirectoryIteratorImpl::makeEntry(absl::string_vie // If we confirm this with an lstat, treat this file entity as // a regular file, which may be unlink()'ed. if (::lstat(full_path.c_str(), &stat_buf) == 0 && S_ISLNK(stat_buf.st_mode)) { - return DirectoryEntry{std::string{filename}, FileType::Regular, absl::nullopt}; + return DirectoryEntry{std::string{filename}, FileType::Regular, std::nullopt}; } } return absl::ErrnoToStatus( result.errno_, fmt::format("unable to stat file: '{}' ({})", full_path, result.errno_)); } else if (S_ISDIR(stat_buf.st_mode)) { - return DirectoryEntry{std::string{filename}, FileType::Directory, absl::nullopt}; + return DirectoryEntry{std::string{filename}, FileType::Directory, std::nullopt}; } else if (S_ISREG(stat_buf.st_mode)) { return DirectoryEntry{std::string{filename}, FileType::Regular, static_cast(stat_buf.st_size)}; } else { - return DirectoryEntry{std::string{filename}, FileType::Other, absl::nullopt}; + return DirectoryEntry{std::string{filename}, FileType::Other, std::nullopt}; } } diff --git a/source/common/filesystem/posix/filesystem_impl.cc b/source/common/filesystem/posix/filesystem_impl.cc index 7f95a490b51ab..76dfb8047d98b 100644 --- a/source/common/filesystem/posix/filesystem_impl.cc +++ b/source/common/filesystem/posix/filesystem_impl.cc @@ -138,9 +138,9 @@ static FileType typeFromStat(const struct stat& s) { return FileType::Other; } -static constexpr absl::optional systemTimeFromTimespec(const struct timespec& t) { +static constexpr std::optional systemTimeFromTimespec(const struct timespec& t) { if (t.tv_sec == 0) { - return absl::nullopt; + return std::nullopt; } return timespecToChrono(t); } @@ -367,6 +367,17 @@ bool InstanceImplPosix::illegalPath(const std::string& path) { absl::StartsWith(canonical_path.return_value_, "/proc/") || canonical_path.return_value_ == "/dev" || canonical_path.return_value_ == "/sys" || canonical_path.return_value_ == "/proc") { + +#ifdef __linux__ + // Allow /dev/shm/*, which is a de-facto standard tmpfs location on linux. A common use case is + // to set the bazel sandbox_base to /dev/shm, since /tmp is not always backed by memory. Some + // tests may then need to access files in bazel sandboxes under this directory. + if (absl::StartsWith(canonical_path.return_value_, "/dev/shm/") || + canonical_path.return_value_ == "/dev/shm") { + return false; + } +#endif + return true; } return false; diff --git a/source/common/filesystem/win32/directory_iterator_impl.cc b/source/common/filesystem/win32/directory_iterator_impl.cc index 21abbcfce89f5..aefae84e0bc7a 100644 --- a/source/common/filesystem/win32/directory_iterator_impl.cc +++ b/source/common/filesystem/win32/directory_iterator_impl.cc @@ -23,7 +23,7 @@ DirectoryIteratorImpl::DirectoryIteratorImpl(const std::string& directory_path) if (status_.ok()) { entry_ = makeEntry(find_data); } else { - entry_ = {"", FileType::Other, absl::nullopt}; + entry_ = {"", FileType::Other, std::nullopt}; } } @@ -39,7 +39,7 @@ DirectoryIteratorImpl& DirectoryIteratorImpl::operator++() { const DWORD err = ::GetLastError(); if (ret == 0) { - entry_ = {"", FileType::Other, absl::nullopt}; + entry_ = {"", FileType::Other, std::nullopt}; if (err != ERROR_NO_MORE_FILES) { status_ = absl::UnknownError(fmt::format("unable to iterate directory: {}", err)); } @@ -58,12 +58,12 @@ DirectoryEntry DirectoryIteratorImpl::makeEntry(const WIN32_FIND_DATA& find_data !(find_data.dwReserved0 & IO_REPARSE_TAG_SYMLINK)) { // The file is reparse point and not a symlink, so it can't be // a regular file or a directory - return {std::string(find_data.cFileName), FileType::Other, absl::nullopt}; + return {std::string(find_data.cFileName), FileType::Other, std::nullopt}; } else if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - return {std::string(find_data.cFileName), FileType::Directory, absl::nullopt}; + return {std::string(find_data.cFileName), FileType::Directory, std::nullopt}; } else if ((find_data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && (find_data.dwReserved0 & IO_REPARSE_TAG_SYMLINK)) { - return {std::string(find_data.cFileName), FileType::Regular, absl::nullopt}; + return {std::string(find_data.cFileName), FileType::Regular, std::nullopt}; } else { ULARGE_INTEGER file_size; file_size.LowPart = find_data.nFileSizeLow; diff --git a/source/common/filesystem/win32/filesystem_impl.cc b/source/common/filesystem/win32/filesystem_impl.cc index 6428354fbead8..e933bd63ab07c 100644 --- a/source/common/filesystem/win32/filesystem_impl.cc +++ b/source/common/filesystem/win32/filesystem_impl.cc @@ -120,7 +120,7 @@ static uint64_t fileSizeFromAttributeData(const WIN32_FILE_ATTRIBUTE_DATA& data) return static_cast(file_size.QuadPart); } -static absl::optional systemTimeFromFileTime(const FILETIME& t) { +static std::optional systemTimeFromFileTime(const FILETIME& t) { // `FILETIME` is a 64 bit value representing the number of 100-nanosecond // intervals since January 1, 1601 (UTC). // https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-filetime @@ -136,7 +136,7 @@ static absl::optional systemTimeFromFileTime(const FILETIME& t) { SystemTime ret = windows_file_time_epoch + std::chrono::microseconds{v / 10}; if (ret <= SystemTime{}) { // If the timestamp is before the unix epoch, return nullopt. - return absl::nullopt; + return std::nullopt; } return ret; } @@ -153,7 +153,7 @@ static FileType fileTypeFromAttributeData(const WIN32_FILE_ATTRIBUTE_DATA& data) static Api::IoCallResult fileInfoFromAttributeData(absl::string_view path, const WIN32_FILE_ATTRIBUTE_DATA& data) { - absl::optional sz; + std::optional sz; FileType type = fileTypeFromAttributeData(data); if (type == FileType::Regular) { sz = fileSizeFromAttributeData(data); diff --git a/source/common/filter/BUILD b/source/common/filter/BUILD index e3180f7e42386..9c72bd5f023e2 100644 --- a/source/common/filter/BUILD +++ b/source/common/filter/BUILD @@ -19,7 +19,7 @@ envoy_cc_library( "//envoy/stats:stats_macros", "//envoy/thread_local:thread_local_interface", "//source/common/common:containers_lib", - "//source/common/config:subscription_base_interface", + "//source/common/config:resource_type_helper_lib", "//source/common/config:utility_lib", "//source/common/grpc:common_lib", "//source/common/init:manager_lib", diff --git a/source/common/filter/config_discovery_impl.cc b/source/common/filter/config_discovery_impl.cc index e94e6aef49309..b1fb4e39d5852 100644 --- a/source/common/filter/config_discovery_impl.cc +++ b/source/common/filter/config_discovery_impl.cc @@ -80,20 +80,21 @@ FilterConfigSubscription::FilterConfigSubscription( Upstream::ClusterManager& cluster_manager, const std::string& stat_prefix, FilterConfigProviderManagerImplBase& filter_config_provider_manager, const std::string& subscription_id, absl::Status& creation_status) - : Config::SubscriptionBase( - factory_context.messageValidationContext().dynamicValidationVisitor(), "name"), - filter_config_name_(filter_config_name), + : filter_config_name_(filter_config_name), last_(std::make_shared("", factory_context.timeSource().systemTime())), factory_context_(factory_context), init_target_(fmt::format("FilterConfigSubscription init {}", filter_config_name_), [this]() { start(); }), + resource_type_helper_(factory_context.messageValidationContext().dynamicValidationVisitor(), + "name"), scope_(factory_context.scope().createScope(stat_prefix)), stats_({ALL_EXTENSION_CONFIG_DISCOVERY_STATS(POOL_COUNTER(*scope_))}), filter_config_provider_manager_(filter_config_provider_manager), subscription_id_(subscription_id) { - const auto resource_name = getResourceName(); + const auto resource_name = resource_type_helper_.getResourceName(); auto subscription_or_error = cluster_manager.subscriptionFactory().subscriptionFromConfigSource( - config_source, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, {}); + config_source, Grpc::Common::typeUrl(resource_name), *scope_, *this, + resource_type_helper_.resourceDecoder(), {}); SET_AND_RETURN_IF_NOT_OK(subscription_or_error.status(), creation_status); subscription_ = std::move(*subscription_or_error); } @@ -114,8 +115,9 @@ FilterConfigSubscription::onConfigUpdate(const std::vector( - resources[0].get().resource()); + const auto& filter_config = + Envoy::Protobuf::DynamicCastMessage( + resources[0].get().resource()); if (filter_config.name() != filter_config_name_) { return absl::InvalidArgumentError(fmt::format( "Unexpected resource name in ExtensionConfigDS response: {}", filter_config.name())); @@ -302,7 +304,7 @@ ProtobufTypes::MessagePtr FilterConfigProviderManagerImplBase::dumpEcdsFilterCon filter_config.set_name(ecds_filter->name()); MessageUtil::packFrom(*filter_config.mutable_typed_config(), *ecds_filter->lastConfig()); auto& filter_config_dump = *config_dump->mutable_ecds_filters()->Add(); - filter_config_dump.mutable_ecds_filter()->PackFrom(filter_config); + std::ignore = filter_config_dump.mutable_ecds_filter()->PackFrom(filter_config); filter_config_dump.set_version_info(ecds_filter->lastVersionInfo()); TimestampUtil::systemClockToTimestamp(ecds_filter->lastUpdated(), *(filter_config_dump.mutable_last_updated())); diff --git a/source/common/filter/config_discovery_impl.h b/source/common/filter/config_discovery_impl.h index d95300ae4d863..4c3f6c7481def 100644 --- a/source/common/filter/config_discovery_impl.h +++ b/source/common/filter/config_discovery_impl.h @@ -15,7 +15,7 @@ #include "envoy/stats/stats_macros.h" #include "source/common/common/assert.h" -#include "source/common/config/subscription_base.h" +#include "source/common/config/resource_type_helper.h" #include "source/common/config/utility.h" #include "source/common/init/manager_impl.h" #include "source/common/init/target_impl.h" @@ -117,7 +117,7 @@ class DynamicFilterConfigProviderImpl : public DynamicFilterConfigProviderImplBa } absl::Status onConfigRemoved(Config::ConfigAppliedCb applied_on_all_threads) override { - absl::optional cb; + std::optional cb; if (default_configuration_) { auto cb_or_error = instantiateFilterFactory(*default_configuration_); RETURN_IF_NOT_OK_REF(cb_or_error.status()); @@ -145,7 +145,7 @@ class DynamicFilterConfigProviderImpl : public DynamicFilterConfigProviderImplBa virtual absl::StatusOr instantiateFilterFactory(const Protobuf::Message& message) const PURE; - void update(absl::optional config, Config::ConfigAppliedCb applied_on_all_threads) { + void update(std::optional config, Config::ConfigAppliedCb applied_on_all_threads) { // This call must not capture 'this' as it is invoked on all workers asynchronously. main_config_->tls_->runOnAllThreads( [config](OptRef tls) { tls->config_ = config; }, @@ -161,8 +161,8 @@ class DynamicFilterConfigProviderImpl : public DynamicFilterConfigProviderImplBa } struct ThreadLocalConfig : public ThreadLocal::ThreadLocalObject { - ThreadLocalConfig() : config_{absl::nullopt} {} - absl::optional config_{}; + ThreadLocalConfig() : config_{std::nullopt} {} + std::optional config_{}; }; // Currently applied configuration to ensure that the main thread deletes the last reference to @@ -173,7 +173,7 @@ class DynamicFilterConfigProviderImpl : public DynamicFilterConfigProviderImplBa : tls_(std::make_unique>(tls)) { tls_->set([](Event::Dispatcher&) { return std::make_shared(); }); } - absl::optional current_config_{absl::nullopt}; + std::optional current_config_{std::nullopt}; ThreadLocal::TypedSlotPtr tls_; }; const std::string stat_prefix_; @@ -418,10 +418,9 @@ struct ExtensionConfigDiscoveryStats { * Subscriptions are shared between the filter config providers. The filter config providers are * notified when a new config is accepted. */ -class FilterConfigSubscription - : Config::SubscriptionBase, - Logger::Loggable, - public std::enable_shared_from_this { +class FilterConfigSubscription : public Config::SubscriptionCallbacks, + Logger::Loggable, + public std::enable_shared_from_this { public: static absl::StatusOr> create(const envoy::config::core::v3::ConfigSource& config_source, @@ -483,6 +482,9 @@ class FilterConfigSubscription Server::Configuration::ServerFactoryContext& factory_context_; Init::SharedTargetImpl init_target_; + const Config::ResourceTypeHelper + resource_type_helper_; + bool started_{false}; Stats::ScopeSharedPtr scope_; diff --git a/source/common/formatter/BUILD b/source/common/formatter/BUILD index 5d85575fb4280..30545773683c1 100644 --- a/source/common/formatter/BUILD +++ b/source/common/formatter/BUILD @@ -19,6 +19,7 @@ envoy_cc_library( "substitution_formatter.h", ], deps = [ + ":builtin_command_parser_factory_helper_lib", "//envoy/api:api_interface", "//envoy/formatter:substitution_formatter_interface", "//envoy/stream_info:stream_info_interface", @@ -28,6 +29,9 @@ envoy_cc_library( "//source/common/json:json_loader_lib", "//source/common/json:json_streamer_lib", "//source/common/json:json_utility_lib", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], @@ -43,6 +47,7 @@ envoy_cc_library( "//source/common/config:utility_lib", "//source/common/protobuf", "//source/server:generic_factory_context_lib", + "@abseil-cpp//absl/status:statusor", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -68,6 +73,7 @@ envoy_cc_library( srcs = ["coalesce_formatter.cc"], hdrs = ["coalesce_formatter.h"], deps = [ + ":builtin_command_parser_factory_helper_lib", ":substitution_format_utility_lib", "//envoy/formatter:substitution_formatter_interface", "//envoy/json:json_object_interface", @@ -107,7 +113,9 @@ envoy_cc_library( hdrs = ["stream_info_formatter.h"], rbe_pool = "6gig", deps = [ + ":substitution_format_utility_lib", "//envoy/api:api_interface", + "//envoy/common:exception_lib", "//envoy/formatter:substitution_formatter_interface", "//envoy/runtime:runtime_interface", "//envoy/stream_info:stream_info_interface", @@ -115,13 +123,14 @@ envoy_cc_library( "//source/common/common:assert_lib", "//source/common/common:random_generator_lib", "//source/common/common:utility_lib", - "//source/common/formatter:substitution_format_utility_lib", "//source/common/grpc:common_lib", "//source/common/http:utility_lib", "//source/common/json:json_loader_lib", "//source/common/json:json_utility_lib", "//source/common/protobuf:message_validator_lib", "//source/common/stream_info:utility_lib", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", ], alwayslink = 1, # has factory registration ) @@ -134,3 +143,16 @@ envoy_cc_library( ], alwayslink = 1, # has factory registration ) + +envoy_cc_library( + name = "builtin_command_parser_factory_helper_lib", + srcs = ["builtin_command_parser_factory_helper.cc"], + hdrs = ["builtin_command_parser_factory_helper.h"], + deps = [ + "//envoy/formatter:substitution_formatter_interface", + "//envoy/registry", + "//source/common/common:assert_lib", + "//source/common/common:macros", + ], + alwayslink = 1, # has factory registration +) diff --git a/source/common/formatter/builtin_command_parser_factory_helper.cc b/source/common/formatter/builtin_command_parser_factory_helper.cc new file mode 100644 index 0000000000000..708bd88581fec --- /dev/null +++ b/source/common/formatter/builtin_command_parser_factory_helper.cc @@ -0,0 +1,28 @@ +#include "source/common/formatter/builtin_command_parser_factory_helper.h" + +#include "envoy/registry/registry.h" + +#include "source/common/common/assert.h" +#include "source/common/common/macros.h" + +namespace Envoy { +namespace Formatter { + +const BuiltInCommandParserFactoryHelper::Parsers& +BuiltInCommandParserFactoryHelper::commandParsers() { + CONSTRUCT_ON_FIRST_USE(Parsers, []() { + BuiltInCommandParserFactoryHelper::Parsers parsers; + for (const auto& factory : Envoy::Registry::FactoryRegistry::factories()) { + if (auto parser = factory.second->createCommandParser(); parser == nullptr) { + ENVOY_BUG(false, fmt::format("Null built-in command parser: {}", factory.first)); + continue; + } else { + parsers.push_back(std::move(parser)); + } + } + return parsers; + }()); +} + +} // namespace Formatter +} // namespace Envoy diff --git a/source/common/formatter/builtin_command_parser_factory_helper.h b/source/common/formatter/builtin_command_parser_factory_helper.h new file mode 100644 index 0000000000000..805d8bcf31bea --- /dev/null +++ b/source/common/formatter/builtin_command_parser_factory_helper.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "envoy/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Formatter { + +/** + * Helper class to get all built-in command parsers for a given formatter context. + */ +class BuiltInCommandParserFactoryHelper { +public: + using Factory = BuiltInCommandParserFactory; + using Parsers = std::vector; + + /** + * Get all built-in command parsers for a given formatter context. + * @return Parsers all built-in command parsers for a given formatter context. + */ + static const Parsers& commandParsers(); +}; + +} // namespace Formatter +} // namespace Envoy diff --git a/source/common/formatter/coalesce_formatter.cc b/source/common/formatter/coalesce_formatter.cc index 8a3130b6daf9c..97bb224ca4cad 100644 --- a/source/common/formatter/coalesce_formatter.cc +++ b/source/common/formatter/coalesce_formatter.cc @@ -1,13 +1,14 @@ #include "source/common/formatter/coalesce_formatter.h" #include "source/common/common/fmt.h" +#include "source/common/formatter/builtin_command_parser_factory_helper.h" #include "source/common/json/json_loader.h" namespace Envoy { namespace Formatter { absl::StatusOr CoalesceFormatter::create(absl::string_view json_config, - absl::optional max_length) { + std::optional max_length) { if (json_config.empty()) { return absl::InvalidArgumentError("COALESCE requires a JSON configuration parameter"); } @@ -58,7 +59,7 @@ CoalesceFormatter::parseOperatorEntry(const Json::Object& entry) { // Check if this is a simple string command with command-only and no parameters. auto string_value = entry.asString(); if (string_value.ok()) { - return createFormatterForCommand(string_value.value(), "", absl::nullopt); + return createFormatterForCommand(string_value.value(), "", std::nullopt); } // Otherwise, it should be an object with "command" field. @@ -83,7 +84,7 @@ CoalesceFormatter::parseOperatorEntry(const Json::Object& entry) { param = param_or_error.value(); } - absl::optional entry_max_length; + std::optional entry_max_length; if (entry.hasObject("max_length")) { auto max_length_or_error = entry.getInteger("max_length"); if (!max_length_or_error.ok()) { @@ -101,10 +102,13 @@ CoalesceFormatter::parseOperatorEntry(const Json::Object& entry) { absl::StatusOr CoalesceFormatter::createFormatterForCommand(absl::string_view command, absl::string_view param, - absl::optional max_length) { + std::optional max_length) { // Try built-in command parsers to create the formatter. for (const auto& parser : BuiltInCommandParserFactoryHelper::commandParsers()) { - auto formatter = parser->parse(command, param, max_length); + absl::StatusOr formatter_result = + parser->parse(command, param, max_length); + RETURN_IF_ERROR(formatter_result.status()); + FormatterProviderPtr formatter = std::move(formatter_result).value(); if (formatter != nullptr) { return formatter; } @@ -113,7 +117,7 @@ CoalesceFormatter::createFormatterForCommand(absl::string_view command, absl::st return absl::InvalidArgumentError(fmt::format("unknown command: '{}'", command)); } -absl::optional +std::optional CoalesceFormatter::format(const Context& context, const StreamInfo::StreamInfo& stream_info) const { for (const auto& formatter : formatters_) { auto result = formatter->format(context, stream_info); @@ -124,7 +128,7 @@ CoalesceFormatter::format(const Context& context, const StreamInfo::StreamInfo& return result; } } - return absl::nullopt; + return std::nullopt; } Protobuf::Value CoalesceFormatter::formatValue(const Context& context, diff --git a/source/common/formatter/coalesce_formatter.h b/source/common/formatter/coalesce_formatter.h index d91256e7aaee2..6832f44947123 100644 --- a/source/common/formatter/coalesce_formatter.h +++ b/source/common/formatter/coalesce_formatter.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -10,8 +11,6 @@ #include "source/common/common/statusor.h" #include "source/common/formatter/substitution_format_utility.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Formatter { @@ -45,15 +44,15 @@ class CoalesceFormatter : public FormatterProvider { * @return StatusOr containing the formatter or an error. */ static absl::StatusOr create(absl::string_view json_config, - absl::optional max_length); + std::optional max_length); CoalesceFormatter(std::vector&& formatters, - absl::optional max_length) + std::optional max_length) : formatters_(std::move(formatters)), max_length_(max_length) {} // FormatterProvider interface. - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; @@ -74,10 +73,10 @@ class CoalesceFormatter : public FormatterProvider { */ static absl::StatusOr createFormatterForCommand(absl::string_view command, absl::string_view param, - absl::optional max_length); + std::optional max_length); std::vector formatters_; - absl::optional max_length_; + std::optional max_length_; }; } // namespace Formatter diff --git a/source/common/formatter/http_specific_formatter.cc b/source/common/formatter/http_specific_formatter.cc index fa280feac9a5f..2444ba8590a5b 100644 --- a/source/common/formatter/http_specific_formatter.cc +++ b/source/common/formatter/http_specific_formatter.cc @@ -1,5 +1,11 @@ #include "source/common/formatter/http_specific_formatter.h" +#include +#include +#include +#include +#include + #include "source/common/common/assert.h" #include "source/common/common/empty_string.h" #include "source/common/common/fmt.h" @@ -16,11 +22,15 @@ #include "source/common/runtime/runtime_features.h" #include "source/common/stream_info/utility.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" + namespace Envoy { namespace Formatter { -absl::optional LocalReplyBodyFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional LocalReplyBodyFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return std::string(context.localReplyBody()); } @@ -29,8 +39,8 @@ Protobuf::Value LocalReplyBodyFormatter::formatValue(const Context& context, return ValueUtil::stringValue(std::string(context.localReplyBody())); } -absl::optional AccessLogTypeFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional AccessLogTypeFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return AccessLogType_Name(context.accessLogType()); } @@ -41,7 +51,7 @@ Protobuf::Value AccessLogTypeFormatter::formatValue(const Context& context, HeaderFormatter::HeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length) + std::optional max_length) : main_header_(main_header), alternative_header_(alternative_header), max_length_(max_length) {} const Http::HeaderEntry* HeaderFormatter::findHeader(OptRef headers) const { @@ -60,10 +70,10 @@ const Http::HeaderEntry* HeaderFormatter::findHeader(OptRef HeaderFormatter::format(OptRef headers) const { +std::optional HeaderFormatter::format(OptRef headers) const { const Http::HeaderEntry* header = findHeader(headers); if (!header) { - return absl::nullopt; + return std::nullopt; } absl::string_view val = header->value().getStringView(); @@ -84,11 +94,11 @@ Protobuf::Value HeaderFormatter::formatValue(OptRef heade ResponseHeaderFormatter::ResponseHeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length) + std::optional max_length) : HeaderFormatter(main_header, alternative_header, max_length) {} -absl::optional ResponseHeaderFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional ResponseHeaderFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return HeaderFormatter::format(context.responseHeaders()); } @@ -99,11 +109,11 @@ Protobuf::Value ResponseHeaderFormatter::formatValue(const Context& context, RequestHeaderFormatter::RequestHeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length) + std::optional max_length) : HeaderFormatter(main_header, alternative_header, max_length) {} -absl::optional RequestHeaderFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional RequestHeaderFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return HeaderFormatter::format(context.requestHeaders()); } @@ -114,11 +124,11 @@ Protobuf::Value RequestHeaderFormatter::formatValue(const Context& context, ResponseTrailerFormatter::ResponseTrailerFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length) + std::optional max_length) : HeaderFormatter(main_header, alternative_header, max_length) {} -absl::optional ResponseTrailerFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional ResponseTrailerFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return HeaderFormatter::format(context.responseTrailers()); } @@ -145,8 +155,8 @@ uint64_t HeadersByteSizeFormatter::extractHeadersByteSize( PANIC_DUE_TO_CORRUPT_ENUM; } -absl::optional HeadersByteSizeFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional HeadersByteSizeFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { return absl::StrCat(extractHeadersByteSize(context.requestHeaders(), context.responseHeaders(), context.responseTrailers())); } @@ -170,16 +180,16 @@ Protobuf::Value TraceIDFormatter::formatValue(const Context& context, return ValueUtil::stringValue(trace_id); } -absl::optional TraceIDFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional TraceIDFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { const auto active_span = context.activeSpan(); if (!active_span.has_value()) { - return absl::nullopt; + return std::nullopt; } auto trace_id = active_span->getTraceId(); if (trace_id.empty()) { - return absl::nullopt; + return std::nullopt; } return trace_id; } @@ -197,25 +207,36 @@ Protobuf::Value SpanIDFormatter::formatValue(const Context& context, return ValueUtil::stringValue(span_id); } -absl::optional SpanIDFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional SpanIDFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { const auto active_span = context.activeSpan(); if (!active_span.has_value()) { - return absl::nullopt; + return std::nullopt; } auto span_id = active_span->getSpanId(); if (span_id.empty()) { - return absl::nullopt; + return std::nullopt; } return span_id; } -GrpcStatusFormatter::Format GrpcStatusFormatter::parseFormat(absl::string_view format) { +absl::StatusOr> +GrpcStatusFormatter::create(const std::string& main_header, const std::string& alternative_header, + std::optional max_length, absl::string_view format) { + absl::StatusOr format_or_status = parseFormat(format); + if (!format_or_status.ok()) { + return format_or_status.status(); + } + return std::unique_ptr(new GrpcStatusFormatter( + main_header, alternative_header, max_length, format_or_status.value())); +} + +absl::StatusOr +GrpcStatusFormatter::parseFormat(absl::string_view format) { if (format.empty() || format == "CAMEL_STRING") { return GrpcStatusFormatter::CamelString; } - if (format == "SNAKE_STRING") { return GrpcStatusFormatter::SnakeString; } @@ -223,26 +244,29 @@ GrpcStatusFormatter::Format GrpcStatusFormatter::parseFormat(absl::string_view f return GrpcStatusFormatter::Number; } - throw EnvoyException("GrpcStatusFormatter only supports CAMEL_STRING, SNAKE_STRING or NUMBER."); + return absl::InvalidArgumentError( + fmt::format("GrpcStatusFormatter only supports CAMEL_STRING, SNAKE_STRING or NUMBER. " + "Got: {}", + format)); } GrpcStatusFormatter::GrpcStatusFormatter(const std::string& main_header, const std::string& alternative_header, - absl::optional max_length, Format format) + std::optional max_length, Format format) : HeaderFormatter(main_header, alternative_header, max_length), format_(format) {} -absl::optional GrpcStatusFormatter::format(const Context& context, - const StreamInfo::StreamInfo& info) const { +std::optional GrpcStatusFormatter::format(const Context& context, + const StreamInfo::StreamInfo& info) const { if (!Grpc::Common::isGrpcRequestHeaders( context.requestHeaders().value_or(*Http::StaticEmptyHeaders::get().request_headers))) { - return absl::nullopt; + return std::nullopt; } const auto grpc_status = Grpc::Common::getGrpcStatus( context.responseTrailers().value_or(*Http::StaticEmptyHeaders::get().response_trailers), context.responseHeaders().value_or(*Http::StaticEmptyHeaders::get().response_headers), info, true); if (!grpc_status.has_value()) { - return absl::nullopt; + return std::nullopt; } switch (format_) { case CamelString: { @@ -305,20 +329,20 @@ Protobuf::Value GrpcStatusFormatter::formatValue(const Context& context, } QueryParameterFormatter::QueryParameterFormatter(absl::string_view parameter_key, - absl::optional max_length) + std::optional max_length) : parameter_key_(parameter_key), max_length_(max_length) {} // FormatterProvider -absl::optional QueryParameterFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional QueryParameterFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { const auto request_headers = context.requestHeaders(); if (!request_headers.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto query_params = Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(request_headers->getPathValue()); - absl::optional value = query_params.getFirstValue(parameter_key_); + std::optional value = query_params.getFirstValue(parameter_key_); if (value.has_value() && max_length_.has_value()) { SubstitutionFormatUtils::truncate(value.value(), max_length_.value()); } @@ -331,25 +355,26 @@ QueryParameterFormatter::formatValue(const Context& context, return ValueUtil::optionalStringValue(format(context, stream_info)); } -QueryParametersFormatter::DecodeOption -QueryParametersFormatter::parseDecodeOption(absl::string_view decoding) { - +absl::StatusOr +QueryParametersFormatter::create(absl::string_view decoding, std::optional max_length) { + DecodeOption decode_option; if (decoding.empty() || decoding == "ORIG") { - return DecodeOption::Original; + decode_option = DecodeOption::Original; } else if (decoding == "DECODED") { - return DecodeOption::Decoded; + decode_option = DecodeOption::Decoded; } else { - throw EnvoyException(fmt::format( + return absl::InvalidArgumentError(fmt::format( "Invalid QUERY_PARAMS option: '{}', only 'ORIG'/'DECODED' are allowed", decoding)); } + return std::make_unique(decode_option, max_length); } // FormatterProvider -absl::optional QueryParametersFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional QueryParametersFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { const auto request_headers = context.requestHeaders(); if (!request_headers.has_value()) { - return absl::nullopt; + return std::nullopt; } // Gather query parameters substring from path @@ -357,7 +382,7 @@ absl::optional QueryParametersFormatter::format(const Context& cont auto query_offset = path_view.find('?'); if (query_offset == absl::string_view::npos) { - return absl::nullopt; + return std::nullopt; } std::string query_params = std::string(path_view.substr(query_offset + 1)); @@ -377,13 +402,13 @@ QueryParametersFormatter::formatValue(const Context& context, return ValueUtil::optionalStringValue(format(context, stream_info)); } -absl::optional PathFormatter::format(const Context& context, - const StreamInfo::StreamInfo&) const { +std::optional PathFormatter::format(const Context& context, + const StreamInfo::StreamInfo&) const { absl::string_view path_view; const auto headers = context.requestHeaders(); if (!headers.has_value()) { - return absl::nullopt; + return std::nullopt; } switch (option_) { case OriginalPathOrPath: @@ -401,7 +426,7 @@ absl::optional PathFormatter::format(const Context& context, } if (path_view.empty()) { - return absl::nullopt; + return std::nullopt; } // Strip query parameters if needed. @@ -423,7 +448,7 @@ Protobuf::Value PathFormatter::formatValue(const Context& context, absl::StatusOr PathFormatter::create(absl::string_view with_query, absl::string_view option, - absl::optional max_length) { + std::optional max_length) { bool with_query_bool = true; PathFormatterOption option_enum = OriginalPathOrPath; @@ -460,7 +485,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { FormatterProviderLookupTbl, {{"REQ", // Same as REQUEST_HEADER and used for backward compatibility. {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -468,7 +493,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"REQUEST_HEADER", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -476,7 +501,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"RESP", // Same as RESPONSE_HEADER and used for backward compatibility. {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -484,7 +509,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"RESPONSE_HEADER", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -492,7 +517,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"TRAILER", // Same as RESPONSE_TRAILER and used for backward compatibility. {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -500,7 +525,7 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"RESPONSE_TRAILER", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -508,47 +533,49 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"LOCAL_REPLY_BODY", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique(); }}}, {"ACCESS_LOG_TYPE", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique(); }}}, {"GRPC_STATUS", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique("grpc-status", "", absl::optional(), - GrpcStatusFormatter::parseFormat(format)); + [](absl::string_view format, std::optional) { + auto result = + GrpcStatusFormatter::create("grpc-status", "", std::optional(), format); + THROW_IF_NOT_OK_REF(result.status()); + return std::move(result).value(); }}}, {"GRPC_STATUS_NUMBER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { - return std::make_unique("grpc-status", "", absl::optional(), + [](absl::string_view, std::optional) { + return std::make_unique("grpc-status", "", std::optional(), GrpcStatusFormatter::Number); }}}, {"REQUEST_HEADERS_BYTES", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( HeadersByteSizeFormatter::HeaderType::RequestHeaders); }}}, {"RESPONSE_HEADERS_BYTES", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( HeadersByteSizeFormatter::HeaderType::ResponseHeaders); }}}, {"RESPONSE_TRAILERS_BYTES", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( HeadersByteSizeFormatter::HeaderType::ResponseTrailers); }}}, {"STREAM_INFO_REQ", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format); THROW_IF_NOT_OK_REF(result.status()); return std::make_unique(result.value().first, @@ -556,28 +583,28 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"TRACE_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique(); }}}, {"SPAN_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique(); }}}, {"QUERY_PARAM", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { return std::make_unique(std::string(format), max_length); }}}, {"QUERY_PARAMS", {CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view decoding, absl::optional max_length) { - return std::make_unique( - QueryParametersFormatter::parseDecodeOption(decoding), max_length); + [](absl::string_view decoding, std::optional max_length) { + return THROW_OR_RETURN_VALUE(QueryParametersFormatter::create(decoding, max_length), + FormatterProviderPtr); }}}, {"PATH", {CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { absl::string_view query; absl::string_view option; SubstitutionFormatUtils::parseSubcommand(format, ':', query, option); @@ -586,15 +613,15 @@ BuiltInHttpCommandParser::getKnownFormatters() { }}}, {"COALESCE", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { return THROW_OR_RETURN_VALUE(CoalesceFormatter::create(format, max_length), FormatterProviderPtr); }}}}); } -FormatterProviderPtr BuiltInHttpCommandParser::parse(absl::string_view command, - absl::string_view subcommand, - absl::optional max_length) const { +absl::StatusOr +BuiltInHttpCommandParser::parse(absl::string_view command, absl::string_view subcommand, + std::optional max_length) const { const FormatterProviderLookupTbl& providers = getKnownFormatters(); auto it = providers.find(command); diff --git a/source/common/formatter/http_specific_formatter.h b/source/common/formatter/http_specific_formatter.h index e8954e31e7626..3c0dd9e48228d 100644 --- a/source/common/formatter/http_specific_formatter.h +++ b/source/common/formatter/http_specific_formatter.h @@ -1,11 +1,10 @@ #pragma once -#include +#include #include -#include -#include +#include +#include #include -#include #include "envoy/formatter/substitution_formatter.h" #include "envoy/stream_info/stream_info.h" @@ -14,7 +13,8 @@ #include "source/common/formatter/substitution_format_utility.h" #include "absl/container/flat_hash_map.h" -#include "absl/types/optional.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" namespace Envoy { namespace Formatter { @@ -27,8 +27,8 @@ class LocalReplyBodyFormatter : public FormatterProvider { LocalReplyBodyFormatter() = default; // Formatter::format - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -41,8 +41,8 @@ class AccessLogTypeFormatter : public FormatterProvider { AccessLogTypeFormatter() = default; // Formatter::format - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -50,10 +50,10 @@ class AccessLogTypeFormatter : public FormatterProvider { class HeaderFormatter { public: HeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length); + std::optional max_length); protected: - absl::optional format(OptRef headers) const; + std::optional format(OptRef headers) const; Protobuf::Value formatValue(OptRef headers) const; private: @@ -61,7 +61,7 @@ class HeaderFormatter { Http::LowerCaseString main_header_; Http::LowerCaseString alternative_header_; - absl::optional max_length_; + std::optional max_length_; }; /** @@ -74,8 +74,8 @@ class HeadersByteSizeFormatter : public FormatterProvider { HeadersByteSizeFormatter(const HeaderType header_type); - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; @@ -92,11 +92,11 @@ class HeadersByteSizeFormatter : public FormatterProvider { class RequestHeaderFormatter : public FormatterProvider, HeaderFormatter { public: RequestHeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length); + std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -107,11 +107,11 @@ class RequestHeaderFormatter : public FormatterProvider, HeaderFormatter { class ResponseHeaderFormatter : public FormatterProvider, HeaderFormatter { public: ResponseHeaderFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length); + std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -122,11 +122,11 @@ class ResponseHeaderFormatter : public FormatterProvider, HeaderFormatter { class ResponseTrailerFormatter : public FormatterProvider, HeaderFormatter { public: ResponseTrailerFormatter(absl::string_view main_header, absl::string_view alternative_header, - absl::optional max_length); + std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -136,8 +136,8 @@ class ResponseTrailerFormatter : public FormatterProvider, HeaderFormatter { */ class TraceIDFormatter : public FormatterProvider { public: - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -147,8 +147,8 @@ class TraceIDFormatter : public FormatterProvider { */ class SpanIDFormatter : public FormatterProvider { public: - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; }; @@ -161,34 +161,38 @@ class GrpcStatusFormatter : public FormatterProvider, HeaderFormatter { Number, }; + static absl::StatusOr> + create(const std::string& main_header, const std::string& alternative_header, + std::optional max_length, absl::string_view format); + GrpcStatusFormatter(const std::string& main_header, const std::string& alternative_header, - absl::optional max_length, Format format); + std::optional max_length, Format format); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; - static Format parseFormat(absl::string_view format); - private: + static absl::StatusOr parseFormat(absl::string_view format); + const Format format_; }; class QueryParameterFormatter : public FormatterProvider { public: - QueryParameterFormatter(absl::string_view parameter_key, absl::optional max_length); + QueryParameterFormatter(absl::string_view parameter_key, std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; private: const std::string parameter_key_; - absl::optional max_length_; + std::optional max_length_; }; class QueryParametersFormatter : public FormatterProvider { @@ -198,20 +202,21 @@ class QueryParametersFormatter : public FormatterProvider { Decoded, }; - static DecodeOption parseDecodeOption(absl::string_view decoding); + static absl::StatusOr create(absl::string_view decoding, + std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; - QueryParametersFormatter(DecodeOption option, absl::optional max_length) + QueryParametersFormatter(DecodeOption option, std::optional max_length) : option_(option), max_length_(max_length) {} private: const DecodeOption option_; - absl::optional max_length_; + std::optional max_length_; }; class PathFormatter : public FormatterProvider { @@ -223,21 +228,21 @@ class PathFormatter : public FormatterProvider { }; static absl::StatusOr - create(absl::string_view with_query, absl::string_view option, absl::optional max_length); + create(absl::string_view with_query, absl::string_view option, std::optional max_length); // FormatterProvider - absl::optional format(const Context& context, - const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const Context& context, + const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const Context& context, const StreamInfo::StreamInfo& stream_info) const override; - PathFormatter(bool with_query, PathFormatterOption option, absl::optional max_length) + PathFormatter(bool with_query, PathFormatterOption option, std::optional max_length) : with_query_(with_query), option_(option), max_length_(max_length) {} private: const bool with_query_{}; const PathFormatterOption option_{}; - absl::optional max_length_; + std::optional max_length_; }; class BuiltInHttpCommandParser : public CommandParser { @@ -245,12 +250,13 @@ class BuiltInHttpCommandParser : public CommandParser { BuiltInHttpCommandParser() = default; // CommandParser - FormatterProviderPtr parse(absl::string_view command, absl::string_view subcommand, - absl::optional max_length) const override; + absl::StatusOr parse(absl::string_view command, + absl::string_view subcommand, + std::optional max_length) const override; private: using FormatterProviderCreateFunc = - std::function)>; + std::function)>; using FormatterProviderLookupTbl = absl::flat_hash_map(const Upstream::HostDescriptionConstSharedPtr&)>; + std::function(const Upstream::HostDescriptionConstSharedPtr&)>; -absl::optional formatUpstreamHostsAttempted(const StreamInfo::StreamInfo& stream_info, - const HostStringExtractor& extractor) { +std::optional formatUpstreamHostsAttempted(const StreamInfo::StreamInfo& stream_info, + const HostStringExtractor& extractor) { const auto opt_ref = stream_info.upstreamInfo(); if (!opt_ref.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto& attempted_hosts = opt_ref->upstreamHostsAttempted(); if (attempted_hosts.empty()) { - return absl::nullopt; + return std::nullopt; } std::vector results; results.reserve(attempted_hosts.size()); @@ -44,7 +45,7 @@ absl::optional formatUpstreamHostsAttempted(const StreamInfo::Strea } } if (results.empty()) { - return absl::nullopt; + return std::nullopt; } return absl::StrJoin(results, ","); } @@ -82,16 +83,16 @@ getUpstreamRemoteAddress(const StreamInfo::StreamInfo& stream_info) { MetadataFormatter::MetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length, + std::optional max_length, MetadataFormatter::GetMetadataFunction get_func) : filter_namespace_(filter_namespace), path_(path.begin(), path.end()), max_length_(max_length), get_func_(get_func) {} -absl::optional +std::optional MetadataFormatter::formatMetadata(const envoy::config::core::v3::Metadata& metadata) const { Protobuf::Value value = formatMetadataValue(metadata); if (value.kind_case() == Protobuf::Value::kNullValue) { - return absl::nullopt; + return std::nullopt; } std::string str; @@ -140,10 +141,10 @@ MetadataFormatter::formatMetadataValue(const envoy::config::core::v3::Metadata& return val; } -absl::optional +std::optional MetadataFormatter::format(const StreamInfo::StreamInfo& stream_info) const { auto metadata = get_func_(stream_info); - return (metadata != nullptr) ? formatMetadata(*metadata) : absl::nullopt; + return (metadata != nullptr) ? formatMetadata(*metadata) : std::nullopt; } Protobuf::Value MetadataFormatter::formatValue(const StreamInfo::StreamInfo& stream_info) const { @@ -156,7 +157,7 @@ Protobuf::Value MetadataFormatter::formatValue(const StreamInfo::StreamInfo& str // @htuch. See: https://github.com/envoyproxy/envoy/issues/3006 DynamicMetadataFormatter::DynamicMetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length) + std::optional max_length) : MetadataFormatter(filter_namespace, path, max_length, [](const StreamInfo::StreamInfo& stream_info) { return &stream_info.dynamicMetadata(); @@ -164,7 +165,7 @@ DynamicMetadataFormatter::DynamicMetadataFormatter(absl::string_view filter_name ClusterMetadataFormatter::ClusterMetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length) + std::optional max_length) : MetadataFormatter(filter_namespace, path, max_length, [](const StreamInfo::StreamInfo& stream_info) -> const envoy::config::core::v3::Metadata* { @@ -177,7 +178,7 @@ ClusterMetadataFormatter::ClusterMetadataFormatter(absl::string_view filter_name UpstreamHostMetadataFormatter::UpstreamHostMetadataFormatter( absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length) + std::optional max_length) : MetadataFormatter(filter_namespace, path, max_length, [](const StreamInfo::StreamInfo& stream_info) -> const envoy::config::core::v3::Metadata* { @@ -192,8 +193,8 @@ UpstreamHostMetadataFormatter::UpstreamHostMetadataFormatter( return host->metadata().get(); }) {} -std::unique_ptr -FilterStateFormatter::create(absl::string_view format, absl::optional max_length, +absl::StatusOr> +FilterStateFormatter::create(absl::string_view format, std::optional max_length, bool is_upstream) { absl::string_view key, serialize_type, field_name; static constexpr absl::string_view PLAIN_SERIALIZATION{"PLAIN"}; @@ -202,7 +203,7 @@ FilterStateFormatter::create(absl::string_view format, absl::optional ma SubstitutionFormatUtils::parseSubcommand(format, ':', key, serialize_type, field_name); if (key.empty()) { - throw EnvoyException("Invalid filter state configuration, key cannot be empty."); + return absl::InvalidArgumentError("Invalid filter state configuration, key cannot be empty."); } if (serialize_type.empty()) { @@ -210,21 +211,29 @@ FilterStateFormatter::create(absl::string_view format, absl::optional ma } if (serialize_type != PLAIN_SERIALIZATION && serialize_type != TYPED_SERIALIZATION && serialize_type != FIELD_SERIALIZATION) { - throw EnvoyException("Invalid filter state serialize type, only " - "support PLAIN/TYPED/FIELD."); + return absl::InvalidArgumentError("Invalid filter state serialize type, only " + "support PLAIN/TYPED/FIELD."); } if ((serialize_type == FIELD_SERIALIZATION) ^ !field_name.empty()) { - throw EnvoyException("Invalid filter state serialize type, FIELD " - "should be used with the field name."); + return absl::InvalidArgumentError("Invalid filter state serialize type, FIELD " + "should be used with the field name."); } const bool serialize_as_string = serialize_type == PLAIN_SERIALIZATION; - return std::make_unique(key, max_length, serialize_as_string, is_upstream, - field_name); + return std::unique_ptr( + new FilterStateFormatter(key, max_length, serialize_as_string, is_upstream, field_name)); } -FilterStateFormatter::FilterStateFormatter(absl::string_view key, absl::optional max_length, +absl::StatusOr> +FilterStateFormatter::createForTest(absl::string_view key, std::optional max_length, + bool serialize_as_string, bool is_upstream, + absl::string_view field_name) { + return std::unique_ptr( + new FilterStateFormatter(key, max_length, serialize_as_string, is_upstream, field_name)); +} + +FilterStateFormatter::FilterStateFormatter(absl::string_view key, std::optional max_length, bool serialize_as_string, bool is_upstream, absl::string_view field_name) : key_(key), max_length_(max_length), is_upstream_(is_upstream) { @@ -258,31 +267,31 @@ FilterStateFormatter::filterState(const StreamInfo::StreamInfo& stream_info) con } struct StringFieldVisitor { - absl::optional operator()(int64_t val) { return absl::StrCat(val); } - absl::optional operator()(absl::string_view val) { return std::string(val); } - absl::optional operator()(absl::monostate) { return {}; } + std::optional operator()(int64_t val) { return absl::StrCat(val); } + std::optional operator()(absl::string_view val) { return std::string(val); } + std::optional operator()(absl::monostate) { return {}; } }; -absl::optional +std::optional FilterStateFormatter::format(const StreamInfo::StreamInfo& stream_info) const { const Envoy::StreamInfo::FilterState::Object* state = filterState(stream_info); if (!state) { - return absl::nullopt; + return std::nullopt; } switch (format_) { case FilterStateFormat::String: { - absl::optional plain_value = state->serializeAsString(); + std::optional plain_value = state->serializeAsString(); if (plain_value.has_value()) { SubstitutionFormatUtils::truncate(plain_value.value(), max_length_); return plain_value.value(); } - return absl::nullopt; + return std::nullopt; } case FilterStateFormat::Proto: { ProtobufTypes::MessagePtr proto = state->serializeAsProto(); if (proto == nullptr) { - return absl::nullopt; + return std::nullopt; } #if defined(ENVOY_ENABLE_FULL_PROTOS) @@ -291,27 +300,27 @@ FilterStateFormatter::format(const StreamInfo::StreamInfo& stream_info) const { if (!status.ok()) { // If the message contains an unknown Any (from WASM or Lua), MessageToJsonString will fail. // TODO(lizan): add support of unknown Any. - return absl::nullopt; + return std::nullopt; } SubstitutionFormatUtils::truncate(value, max_length_); return value; #else PANIC("FilterStateFormatter::format requires full proto support"); - return absl::nullopt; + return std::nullopt; #endif } case FilterStateFormat::Field: { auto field_value = state->getField(field_name_); auto string_value = absl::visit(StringFieldVisitor(), field_value); if (!string_value) { - return absl::nullopt; + return std::nullopt; } SubstitutionFormatUtils::truncate(string_value.value(), max_length_); return string_value; } default: - return absl::nullopt; + return std::nullopt; } } @@ -323,7 +332,7 @@ Protobuf::Value FilterStateFormatter::formatValue(const StreamInfo::StreamInfo& switch (format_) { case FilterStateFormat::String: { - absl::optional plain_value = state->serializeAsString(); + std::optional plain_value = state->serializeAsString(); if (plain_value.has_value()) { SubstitutionFormatUtils::truncate(plain_value.value(), max_length_); return ValueUtil::stringValue(plain_value.value()); @@ -361,19 +370,38 @@ Protobuf::Value FilterStateFormatter::formatValue(const StreamInfo::StreamInfo& const absl::flat_hash_map CommonDurationFormatter::KnownTimePointGetters{ {FirstDownstreamRxByteReceived, - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { return stream_info.startTimeMonotonic(); }}, {LastDownstreamRxByteReceived, - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto downstream_timing = stream_info.downstreamTiming(); if (downstream_timing.has_value()) { return downstream_timing->lastDownstreamRxByteReceived(); } return {}; }}, + {DownstreamConnectionBegin, + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { + const auto downstream_timing = stream_info.downstreamTiming(); + if (downstream_timing.has_value()) { + const auto connection_begin = downstream_timing->downstreamConnectionBegin(); + if (connection_begin.has_value()) { + return connection_begin; + } + } + return stream_info.startTimeMonotonic(); + }}, + {DownstreamConnectionEnd, + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { + const auto downstream_timing = stream_info.downstreamTiming(); + if (downstream_timing.has_value()) { + return downstream_timing->downstreamConnectionEnd(); + } + return {}; + }}, {UpstreamConnectStart, - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().upstream_connect_start_; @@ -381,7 +409,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().upstream_connect_complete_; @@ -389,7 +417,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().upstream_handshake_complete_; @@ -397,7 +425,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().first_upstream_tx_byte_sent_; @@ -405,7 +433,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().last_upstream_tx_byte_sent_; @@ -413,7 +441,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().first_upstream_rx_byte_received_; @@ -421,7 +449,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().first_upstream_rx_body_byte_received_; @@ -429,7 +457,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto upstream_info = stream_info.upstreamInfo(); if (upstream_info.has_value()) { return upstream_info->upstreamTiming().last_upstream_rx_byte_received_; @@ -437,7 +465,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto downstream_timing = stream_info.downstreamTiming(); if (downstream_timing.has_value()) { return downstream_timing->firstDownstreamTxByteSent(); @@ -445,7 +473,7 @@ const absl::flat_hash_map absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto downstream_timing = stream_info.downstreamTiming(); if (downstream_timing.has_value()) { return downstream_timing->lastDownstreamTxByteSent(); @@ -466,17 +494,18 @@ CommonDurationFormatter::getTimePointGetterByName(absl::string_view name) { if (downstream_timing.has_value()) { return downstream_timing->getValue(key); } - return absl::optional{}; + return std::optional{}; }; } -std::unique_ptr +absl::StatusOr> CommonDurationFormatter::create(absl::string_view sub_command) { // Split the sub_command by ':'. absl::InlinedVector parsed_sub_commands = absl::StrSplit(sub_command, ':'); if (parsed_sub_commands.size() < 2 || parsed_sub_commands.size() > 3) { - throw EnvoyException(fmt::format("Invalid common duration configuration: {}.", sub_command)); + return absl::InvalidArgumentError( + fmt::format("Invalid common duration configuration: {}.", sub_command)); } absl::string_view start = parsed_sub_commands[0]; @@ -494,28 +523,29 @@ CommonDurationFormatter::create(absl::string_view sub_command) { } else if (precision_str == NanosecondsPrecision) { precision = DurationPrecision::Nanoseconds; } else { - throw EnvoyException(fmt::format("Invalid common duration precision: {}.", precision_str)); + return absl::InvalidArgumentError( + fmt::format("Invalid common duration precision: {}.", precision_str)); } } TimePointGetter start_getter = getTimePointGetterByName(start); TimePointGetter end_getter = getTimePointGetterByName(end); - return std::make_unique(std::move(start_getter), std::move(end_getter), - precision); + return std::unique_ptr( + new CommonDurationFormatter(std::move(start_getter), std::move(end_getter), precision)); } -absl::optional +std::optional CommonDurationFormatter::getDurationCount(const StreamInfo::StreamInfo& info) const { auto time_point_beg = time_point_beg_(info); auto time_point_end = time_point_end_(info); if (!time_point_beg.has_value() || !time_point_end.has_value()) { - return absl::nullopt; + return std::nullopt; } if (time_point_end.value() < time_point_beg.value()) { - return absl::nullopt; + return std::nullopt; } auto duration = time_point_end.value() - time_point_beg.value(); @@ -531,11 +561,11 @@ CommonDurationFormatter::getDurationCount(const StreamInfo::StreamInfo& info) co PANIC("Invalid duration precision"); } -absl::optional +std::optional CommonDurationFormatter::format(const StreamInfo::StreamInfo& info) const { auto duration = getDurationCount(info); if (!duration.has_value()) { - return absl::nullopt; + return std::nullopt; } return fmt::format_int(duration.value()).str(); } @@ -552,71 +582,78 @@ Protobuf::Value CommonDurationFormatter::formatValue(const StreamInfo::StreamInf StartTimeFormatter::StartTimeFormatter(absl::string_view format) : SystemTimeFormatter( format, std::make_unique( - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { return stream_info.startTime(); })) {} DownstreamPeerCertVStartFormatter::DownstreamPeerCertVStartFormatter(absl::string_view format) : SystemTimeFormatter( format, std::make_unique( - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto connection_info = stream_info.downstreamAddressProvider().sslConnection(); return connection_info != nullptr ? connection_info->validFromPeerCertificate() - : absl::optional(); + : std::optional(); })) {} DownstreamPeerCertVEndFormatter::DownstreamPeerCertVEndFormatter(absl::string_view format) : SystemTimeFormatter( format, std::make_unique( - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { const auto connection_info = stream_info.downstreamAddressProvider().sslConnection(); return connection_info != nullptr ? connection_info->expirationPeerCertificate() - : absl::optional(); + : std::optional(); })) {} UpstreamPeerCertVStartFormatter::UpstreamPeerCertVStartFormatter(absl::string_view format) : SystemTimeFormatter( format, std::make_unique( - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { return stream_info.upstreamInfo() && stream_info.upstreamInfo()->upstreamSslConnection() != nullptr ? stream_info.upstreamInfo() ->upstreamSslConnection() ->validFromPeerCertificate() - : absl::optional(); + : std::optional(); })) {} UpstreamPeerCertVEndFormatter::UpstreamPeerCertVEndFormatter(absl::string_view format) : SystemTimeFormatter( format, std::make_unique( - [](const StreamInfo::StreamInfo& stream_info) -> absl::optional { + [](const StreamInfo::StreamInfo& stream_info) -> std::optional { return stream_info.upstreamInfo() && stream_info.upstreamInfo()->upstreamSslConnection() != nullptr ? stream_info.upstreamInfo() ->upstreamSslConnection() ->expirationPeerCertificate() - : absl::optional(); + : std::optional(); })) {} +absl::Status SystemTimeFormatter::checkConstructPreconditions(absl::string_view format) { + // Validate the input specifier here. The formatted string may be destined for a header, and + // should not contain invalid characters {NUL, LR, CF}. + if (RE2::PartialMatch(format, getSystemTimeFormatNewlinePattern())) { + return absl::InvalidArgumentError( + "Invalid header configuration. Format string contains newline."); + } + return absl::OkStatus(); +} + SystemTimeFormatter::SystemTimeFormatter(absl::string_view format, TimeFieldExtractorPtr f, bool local_time) : date_formatter_(format, local_time), time_field_extractor_(std::move(f)), local_time_(local_time) { - // Validate the input specifier here. The formatted string may be destined for a header, and - // should not contain invalid characters {NUL, LR, CF}. - if (re2::RE2::PartialMatch(format, getSystemTimeFormatNewlinePattern())) { - throw EnvoyException("Invalid header configuration. Format string contains newline."); - } + // Sanity checking that pre-constructor validation was not skipped. + ASSERT(checkConstructPreconditions(format).ok()); } -absl::optional +std::optional SystemTimeFormatter::format(const StreamInfo::StreamInfo& stream_info) const { const auto time_field = (*time_field_extractor_)(stream_info); if (!time_field.has_value()) { - return absl::nullopt; + return std::nullopt; } if (date_formatter_.formatString().empty()) { return AccessLogDateTimeFormatter::fromTime(time_field.value(), local_time_); @@ -629,7 +666,7 @@ Protobuf::Value SystemTimeFormatter::formatValue(const StreamInfo::StreamInfo& s } EnvironmentFormatter::EnvironmentFormatter(absl::string_view key, - absl::optional max_length) { + std::optional max_length) { ASSERT(!key.empty()); const std::string key_str = std::string(key); @@ -643,16 +680,19 @@ EnvironmentFormatter::EnvironmentFormatter(absl::string_view key, str_.set_string_value(DefaultUnspecifiedValueString); } -absl::optional EnvironmentFormatter::format(const StreamInfo::StreamInfo&) const { +std::optional EnvironmentFormatter::format(const StreamInfo::StreamInfo&) const { return str_.string_value(); } Protobuf::Value EnvironmentFormatter::formatValue(const StreamInfo::StreamInfo&) const { return str_; } -RequestedServerNameFormatter::RequestedServerNameFormatter(absl::string_view source, - absl::string_view option) { +RequestedServerNameFormatter::RequestedServerNameFormatter(HostFormatterSource source, + HostFormatterOption option) + : source_(source), option_(option) {} +absl::StatusOr> +RequestedServerNameFormatter::create(absl::string_view source, absl::string_view option) { HostFormatterSource host_source = SNI; HostFormatterOption option_enum = OriginalHostOrHost; @@ -665,9 +705,10 @@ RequestedServerNameFormatter::RequestedServerNameFormatter(absl::string_view sou } else if (source.empty()) { host_source = SNI; } else { - throw EnvoyException(fmt::format("Invalid REQUESTED_SERVER_NAME option: '{}', only " - "'SNI_ONLY'/'SNI_FIRST'/'HOST_FIRST' are allowed", - source)); + return absl::InvalidArgumentError( + fmt::format("Invalid REQUESTED_SERVER_NAME option: '{}', only " + "'SNI_ONLY'/'SNI_FIRST'/'HOST_FIRST' are allowed", + source)); } if (option == "ORIG_OR_HOST") { @@ -679,17 +720,18 @@ RequestedServerNameFormatter::RequestedServerNameFormatter(absl::string_view sou } else if (option.empty()) { option_enum = OriginalHostOrHost; } else { - throw EnvoyException(fmt::format("Invalid REQUESTED_SERVER_NAME option: '{}', only " - "'ORIG_OR_HOST'/'HOST'/'ORIG' are allowed", - option)); + return absl::InvalidArgumentError( + fmt::format("Invalid REQUESTED_SERVER_NAME option: '{}', only " + "'ORIG_OR_HOST'/'HOST'/'ORIG' are allowed", + option)); } - source_ = host_source; - option_ = option_enum; + return std::unique_ptr( + new RequestedServerNameFormatter(host_source, option_enum)); } -absl::optional +std::optional RequestedServerNameFormatter::format(const StreamInfo::StreamInfo& stream_info) const { - absl::optional result; + std::optional result; switch (source_) { case SNI: result = getSNIFromStreamInfo(stream_info); @@ -710,9 +752,9 @@ RequestedServerNameFormatter::format(const StreamInfo::StreamInfo& stream_info) return result; } -absl::optional RequestedServerNameFormatter::getSNIFromStreamInfo( +std::optional RequestedServerNameFormatter::getSNIFromStreamInfo( const StreamInfo::StreamInfo& stream_info) const { - absl::optional result; + std::optional result; if (!stream_info.downstreamAddressProvider().requestedServerName().empty()) { result = StringUtil::sanitizeInvalidHostname( stream_info.downstreamAddressProvider().requestedServerName()); @@ -720,24 +762,33 @@ absl::optional RequestedServerNameFormatter::getSNIFromStreamInfo( return result; } -absl::optional +std::optional RequestedServerNameFormatter::getHostFromHeaders(const StreamInfo::StreamInfo& stream_info) const { - absl::optional result; - const auto& headers = stream_info.getRequestHeaders(); + std::optional result; + const auto* headers = stream_info.getRequestHeaders(); if (headers != nullptr) { switch (option_) { - case HostOnly: - result = headers->Host()->value().getStringView(); + case HostOnly: { + if (auto host = headers->Host(); host != nullptr) { + result = host->value().getStringView(); + } break; - case OriginalHostOnly: - result = headers->EnvoyOriginalHost()->value().getStringView(); + } + case OriginalHostOnly: { + if (auto orig = headers->EnvoyOriginalHost(); orig != nullptr) { + result = orig->value().getStringView(); + } break; - case OriginalHostOrHost: - result = headers->EnvoyOriginalHost() != nullptr - ? headers->EnvoyOriginalHost()->value().getStringView() - : headers->Host()->value().getStringView(); + } + case OriginalHostOrHost: { + if (auto orig = headers->EnvoyOriginalHost(); orig != nullptr) { + result = orig->value().getStringView(); + } else if (auto host = headers->Host(); host != nullptr) { + result = host->value().getStringView(); + } break; } + } } return result; } @@ -750,7 +801,7 @@ RequestedServerNameFormatter::formatValue(const StreamInfo::StreamInfo& stream_i // StreamInfo std::string formatter provider. class StreamInfoStringFormatterProvider : public StreamInfoFormatterProvider { public: - using FieldExtractor = std::function(const StreamInfo::StreamInfo&)>; + using FieldExtractor = std::function(const StreamInfo::StreamInfo&)>; StreamInfoStringFormatterProvider(FieldExtractor f) : field_extractor_(f) {} @@ -758,7 +809,7 @@ class StreamInfoStringFormatterProvider : public StreamInfoFormatterProvider { // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { return field_extractor_(stream_info); } Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { @@ -773,7 +824,7 @@ class StreamInfoStringFormatterProvider : public StreamInfoFormatterProvider { class StreamInfoDurationFormatterProvider : public StreamInfoFormatterProvider { public: using FieldExtractor = - std::function(const StreamInfo::StreamInfo&)>; + std::function(const StreamInfo::StreamInfo&)>; StreamInfoDurationFormatterProvider(FieldExtractor f) : field_extractor_(f) {} @@ -781,10 +832,10 @@ class StreamInfoDurationFormatterProvider : public StreamInfoFormatterProvider { // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { const auto millis = extractMillis(stream_info); if (!millis) { - return absl::nullopt; + return std::nullopt; } return fmt::format_int(millis.value()).str(); @@ -799,12 +850,12 @@ class StreamInfoDurationFormatterProvider : public StreamInfoFormatterProvider { } private: - absl::optional extractMillis(const StreamInfo::StreamInfo& stream_info) const { + std::optional extractMillis(const StreamInfo::StreamInfo& stream_info) const { const auto time = field_extractor_(stream_info); if (time) { return std::chrono::duration_cast(time.value()).count(); } - return absl::nullopt; + return std::nullopt; } FieldExtractor field_extractor_; @@ -821,7 +872,7 @@ class StreamInfoUInt64FormatterProvider : public StreamInfoFormatterProvider { // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { return fmt::format_int(field_extractor_(stream_info)).str(); } Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override { @@ -844,7 +895,7 @@ class StreamInfoAddressFormatterProvider : public StreamInfoFormatterProvider { } static std::unique_ptr - withoutPort(FieldExtractor f, absl::optional mask_prefix_len = absl::nullopt) { + withoutPort(FieldExtractor f, std::optional mask_prefix_len = std::nullopt) { return std::make_unique( f, StreamInfoAddressFieldExtractionType::WithoutPort, mask_prefix_len); } @@ -861,17 +912,17 @@ class StreamInfoAddressFormatterProvider : public StreamInfoFormatterProvider { StreamInfoAddressFormatterProvider(FieldExtractor f, StreamInfoAddressFieldExtractionType extraction_type, - absl::optional mask_prefix_len = absl::nullopt) + std::optional mask_prefix_len = std::nullopt) : field_extractor_(f), extraction_type_(extraction_type), mask_prefix_len_(mask_prefix_len) {} // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { Network::Address::InstanceConstSharedPtr address = field_extractor_(stream_info); if (!address) { - return absl::nullopt; + return std::nullopt; } return toString(*address); @@ -910,14 +961,14 @@ class StreamInfoAddressFormatterProvider : public StreamInfoFormatterProvider { FieldExtractor field_extractor_; const StreamInfoAddressFieldExtractionType extraction_type_; - const absl::optional mask_prefix_len_; + const std::optional mask_prefix_len_; }; // Ssl::ConnectionInfo std::string field extractor. class StreamInfoSslConnectionInfoFormatterProvider : public StreamInfoFormatterProvider { public: using FieldExtractor = - std::function(const Ssl::ConnectionInfo& connection_info)>; + std::function(const Ssl::ConnectionInfo& connection_info)>; StreamInfoSslConnectionInfoFormatterProvider(FieldExtractor f) : field_extractor_(f) {} @@ -925,14 +976,14 @@ class StreamInfoSslConnectionInfoFormatterProvider : public StreamInfoFormatterP // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { if (stream_info.downstreamAddressProvider().sslConnection() == nullptr) { - return absl::nullopt; + return std::nullopt; } const auto value = field_extractor_(*stream_info.downstreamAddressProvider().sslConnection()); if (value && value->empty()) { - return absl::nullopt; + return std::nullopt; } return value; @@ -958,7 +1009,7 @@ class StreamInfoSslConnectionInfoFormatterProvider : public StreamInfoFormatterP class StreamInfoUpstreamSslConnectionInfoFormatterProvider : public StreamInfoFormatterProvider { public: using FieldExtractor = - std::function(const Ssl::ConnectionInfo& connection_info)>; + std::function(const Ssl::ConnectionInfo& connection_info)>; StreamInfoUpstreamSslConnectionInfoFormatterProvider(FieldExtractor f) : field_extractor_(f) {} @@ -966,15 +1017,15 @@ class StreamInfoUpstreamSslConnectionInfoFormatterProvider : public StreamInfoFo // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const StreamInfo::StreamInfo& stream_info) const override { if (!stream_info.upstreamInfo() || stream_info.upstreamInfo()->upstreamSslConnection() == nullptr) { - return absl::nullopt; + return std::nullopt; } const auto value = field_extractor_(*(stream_info.upstreamInfo()->upstreamSslConnection())); if (value && value->empty()) { - return absl::nullopt; + return std::nullopt; } return value; @@ -1007,7 +1058,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide { {"REQUEST_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); @@ -1016,7 +1067,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"REQUEST_TX_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); @@ -1025,7 +1076,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"RESPONSE_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); @@ -1034,14 +1085,14 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"RESPONSE_TX_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); auto downstream = timing.lastDownstreamTxByteSent(); auto upstream = timing.firstUpstreamRxByteReceived(); - absl::optional result; + std::optional result; if (downstream && upstream) { result = downstream.value() - upstream.value(); } @@ -1051,7 +1102,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_HANDSHAKE_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); @@ -1060,7 +1111,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"ROUNDTRIP_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { StreamInfo::TimingUtility timing(stream_info); @@ -1069,7 +1120,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.bytesReceived(); @@ -1077,7 +1128,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"BYTES_RETRANSMITTED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.bytesRetransmitted(); @@ -1085,7 +1136,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"PACKETS_RETRANSMITTED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.packetsRetransmitted(); @@ -1093,7 +1144,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_WIRE_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1103,7 +1154,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HEADER_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1113,7 +1164,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1125,7 +1176,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_WIRE_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1135,7 +1186,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_HEADER_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1145,7 +1196,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_RECEIVED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1157,7 +1208,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"PROTOCOL", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return SubstitutionFormatUtils::protocolToString( @@ -1166,19 +1217,19 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PROTOCOL", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.upstreamInfo() ? SubstitutionFormatUtils::protocolToString( stream_info.upstreamInfo() ->upstreamProtocol()) - : absl::nullopt; + : std::nullopt; }); }}}, {"RESPONSE_CODE", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.responseCode().value_or(0); @@ -1186,7 +1237,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"RESPONSE_CODE_DETAILS", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { + [](absl::string_view format, std::optional) { bool allow_whitespaces = (format == "ALLOW_WHITESPACES"); return std::make_unique( [allow_whitespaces]( @@ -1195,14 +1246,14 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide !stream_info.responseCodeDetails().has_value()) { return stream_info.responseCodeDetails(); } - return absl::optional( + return std::optional( StringUtil::replaceAllEmptySpace( stream_info.responseCodeDetails().value())); }); }}}, {"CONNECTION_TERMINATION_DETAILS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.connectionTerminationDetails(); @@ -1210,7 +1261,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.bytesSent(); @@ -1218,7 +1269,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_WIRE_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1228,7 +1279,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HEADER_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1238,7 +1289,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_DECOMPRESSED_HEADER_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1250,7 +1301,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_WIRE_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1260,7 +1311,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_HEADER_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1270,7 +1321,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DECOMPRESSED_HEADER_BYTES_SENT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { const auto& bytes_meter = @@ -1282,7 +1333,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.currentDuration(); @@ -1290,12 +1341,12 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"COMMON_DURATION", {CommandSyntaxChecker::PARAMS_REQUIRED, - [](absl::string_view sub_command, absl::optional) { + [](absl::string_view sub_command, std::optional) { return CommonDurationFormatter::create(sub_command); }}}, {"CUSTOM_FLAGS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return std::string(stream_info.customFlags()); @@ -1303,7 +1354,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"RESPONSE_FLAGS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return StreamInfo::ResponseFlagUtils::toShortString( @@ -1312,7 +1363,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"RESPONSE_FLAGS_LONG", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return StreamInfo::ResponseFlagUtils::toString(stream_info); @@ -1320,40 +1371,39 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HOST_NAME", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { const auto opt_ref = stream_info.upstreamInfo(); if (!opt_ref.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto host = opt_ref->upstreamHost(); if (host == nullptr) { - return absl::nullopt; + return std::nullopt; } std::string host_name = host->hostname(); if (host_name.empty()) { // If no hostname is available, the main address is used. return host->address()->asString(); } - return absl::make_optional( - std::move(host_name)); + return std::make_optional(std::move(host_name)); }); }}}, {"UPSTREAM_HOST_NAME_WITHOUT_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { const auto opt_ref = stream_info.upstreamInfo(); if (!opt_ref.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto host = opt_ref->upstreamHost(); if (host == nullptr) { - return absl::nullopt; + return std::nullopt; } std::string host_name = host->hostname(); if (host_name.empty()) { @@ -1361,13 +1411,12 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide host_name = host->address()->asString(); } Envoy::Http::HeaderUtility::stripPortFromHost(host_name); - return absl::make_optional( - std::move(host_name)); + return std::make_optional(std::move(host_name)); }); }}}, {"UPSTREAM_HOST", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1384,15 +1433,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HOSTS_ATTEMPTED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return formatUpstreamHostsAttempted( stream_info, [](const Upstream::HostDescriptionConstSharedPtr& host) - -> absl::optional { + -> std::optional { if (host == nullptr || host->address() == nullptr) { - return absl::nullopt; + return std::nullopt; } return host->address()->asString(); }); @@ -1400,15 +1449,15 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HOSTS_ATTEMPTED_WITHOUT_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return formatUpstreamHostsAttempted( stream_info, [](const Upstream::HostDescriptionConstSharedPtr& host) - -> absl::optional { + -> std::optional { if (host == nullptr || host->address() == nullptr) { - return absl::nullopt; + return std::nullopt; } return StreamInfo::Utility:: formatDownstreamAddressNoPort(*host->address()); @@ -1417,37 +1466,37 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_HOST_NAMES_ATTEMPTED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return formatUpstreamHostsAttempted( stream_info, [](const Upstream::HostDescriptionConstSharedPtr& host) - -> absl::optional { + -> std::optional { if (host == nullptr) { - return absl::nullopt; + return std::nullopt; } std::string host_name = host->hostname(); if (host_name.empty() && host->address() != nullptr) { host_name = host->address()->asString(); } return host_name.empty() - ? absl::nullopt - : absl::make_optional(host_name); + ? std::nullopt + : std::make_optional(host_name); }); }); }}}, {"UPSTREAM_HOST_NAMES_ATTEMPTED_WITHOUT_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return formatUpstreamHostsAttempted( stream_info, [](const Upstream::HostDescriptionConstSharedPtr& host) - -> absl::optional { + -> std::optional { if (host == nullptr) { - return absl::nullopt; + return std::nullopt; } std::string host_name = host->hostname(); if (host_name.empty() && host->address() != nullptr) { @@ -1456,14 +1505,14 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide Envoy::Http::HeaderUtility::stripPortFromHost( host_name); return host_name.empty() - ? absl::nullopt - : absl::make_optional(host_name); + ? std::nullopt + : std::make_optional(host_name); }); }); }}}, {"UPSTREAM_CONNECTION_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { uint64_t upstream_connection_id = 0; @@ -1477,18 +1526,18 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_CONNECTION_IDS_ATTEMPTED", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { const auto opt_ref = stream_info.upstreamInfo(); if (!opt_ref.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto& attempted_ids = opt_ref->upstreamConnectionIdsAttempted(); if (attempted_ids.empty()) { - return absl::nullopt; + return std::nullopt; } std::vector ids; ids.reserve(attempted_ids.size()); @@ -1500,7 +1549,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_CLUSTER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { std::string upstream_cluster_name; @@ -1510,14 +1559,14 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide } return upstream_cluster_name.empty() - ? absl::nullopt - : absl::make_optional( + ? std::nullopt + : std::make_optional( upstream_cluster_name); }); }}}, {"UPSTREAM_CLUSTER_RAW", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { std::string upstream_cluster_name; @@ -1527,14 +1576,14 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide } return upstream_cluster_name.empty() - ? absl::nullopt - : absl::make_optional( + ? std::nullopt + : std::make_optional( upstream_cluster_name); }); }}}, {"UPSTREAM_LOCAL_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1549,8 +1598,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_LOCAL_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1572,7 +1621,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_LOCAL_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1587,7 +1636,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_REMOTE_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1596,8 +1645,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_REMOTE_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1613,7 +1662,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_REMOTE_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1622,7 +1671,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_REMOTE_ADDRESS_ENDPOINT_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justEndpointId( [](const StreamInfo::StreamInfo& stream_info) -> Network::Address::InstanceConstSharedPtr { @@ -1631,7 +1680,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_REQUEST_ATTEMPT_COUNT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.attemptCount().value_or(0); @@ -1639,16 +1688,26 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_TLS_CIPHER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { return connection_info.ciphersuiteString(); }); }}}, + {"UPSTREAM_TLS_GROUP", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, std::optional) { + return std::make_unique< + StreamInfoUpstreamSslConnectionInfoFormatterProvider>( + [](const Ssl::ConnectionInfo& connection_info) { + return std::make_optional( + connection_info.tlsGroupString()); + }); + }}}, {"UPSTREAM_TLS_VERSION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1657,16 +1716,35 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_TLS_SESSION_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { return connection_info.sessionId(); }); }}}, + {"UPSTREAM_SERVER_NAME", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, std::optional) { + return std::make_unique( + [](const StreamInfo::StreamInfo& stream_info) + -> std::optional { + if (stream_info.upstreamInfo() && + stream_info.upstreamInfo()->upstreamSslConnection() != + nullptr) { + auto sni = stream_info.upstreamInfo() + ->upstreamSslConnection() + ->sni(); + if (!sni.empty()) { + return std::string(sni); + } + } + return std::nullopt; + }); + }}}, {"UPSTREAM_PEER_ISSUER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1675,7 +1753,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PEER_CERT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1684,7 +1762,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PEER_SUBJECT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1693,7 +1771,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1702,17 +1780,17 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DETECTED_CLOSE_TYPE", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return detectedCloseTypeToString( stream_info.downstreamDetectedCloseType()); }); }}}, {"DOWNSTREAM_DIRECT_LOCAL_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1721,8 +1799,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1738,8 +1816,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_LOCAL_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1755,7 +1833,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1764,7 +1842,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_LOCAL_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1773,7 +1851,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_ADDRESS_ENDPOINT_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justEndpointId( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1782,7 +1860,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_LOCAL_ADDRESS_ENDPOINT_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justEndpointId( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1791,7 +1869,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_REMOTE_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1800,8 +1878,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1817,7 +1895,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_REMOTE_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1826,7 +1904,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_REMOTE_ADDRESS", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::withPort( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1835,8 +1913,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_REMOTE_ADDRESS_WITHOUT_PORT", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - absl::optional mask_prefix_len; + [](absl::string_view format, std::optional) { + std::optional mask_prefix_len; if (!format.empty()) { int len; if (absl::SimpleAtoi(format, &len)) { @@ -1852,7 +1930,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_DIRECT_REMOTE_PORT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return StreamInfoAddressFormatterProvider::justPort( [](const Envoy::StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1861,7 +1939,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"CONNECTION_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { return stream_info.downstreamAddressProvider() @@ -1871,20 +1949,19 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"REQUESTED_SERVER_NAME", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { + [](absl::string_view format, std::optional) { absl::string_view fallback; absl::string_view option; SubstitutionFormatUtils::parseSubcommand(format, ':', fallback, option); - return std::make_unique(fallback, - option); + return RequestedServerNameFormatter::create(fallback, option); }}}, {"ROUTE_NAME", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; std::string route_name = stream_info.getRouteName(); if (!route_name.empty()) { result = route_name; @@ -1894,7 +1971,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PEER_URI_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1904,7 +1981,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PEER_DNS_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1914,7 +1991,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_PEER_IP_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1924,7 +2001,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_LOCAL_URI_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1934,7 +2011,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_LOCAL_DNS_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1944,7 +2021,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_LOCAL_IP_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoUpstreamSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1954,7 +2031,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_URI_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1964,7 +2041,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_DNS_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1974,7 +2051,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_IP_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1984,7 +2061,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_EMAIL_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -1994,7 +2071,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_OTHERNAME_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2004,7 +2081,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_URI_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2014,7 +2091,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_DNS_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2024,7 +2101,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_IP_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2034,7 +2111,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_EMAIL_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2044,7 +2121,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_OTHERNAME_SAN", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2054,7 +2131,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_SUBJECT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2063,7 +2140,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_SUBJECT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2072,7 +2149,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_TLS_SESSION_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2081,16 +2158,26 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_TLS_CIPHER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { return connection_info.ciphersuiteString(); }); }}}, + {"DOWNSTREAM_TLS_GROUP", + {CommandSyntaxChecker::COMMAND_ONLY, + [](absl::string_view, std::optional) { + return std::make_unique< + StreamInfoSslConnectionInfoFormatterProvider>( + [](const Ssl::ConnectionInfo& connection_info) { + return std::make_optional( + connection_info.tlsGroupString()); + }); + }}}, {"DOWNSTREAM_TLS_VERSION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2099,7 +2186,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_FINGERPRINT_256", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2108,7 +2195,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_FINGERPRINT_1", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2117,7 +2204,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_SERIAL", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2126,7 +2213,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256", {CommandSyntaxChecker::COMMAND_ONLY, - [](const absl::string_view, absl::optional) { + [](const absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2137,7 +2224,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1", {CommandSyntaxChecker::COMMAND_ONLY, - [](const absl::string_view, absl::optional) { + [](const absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2147,7 +2234,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_CHAIN_SERIALS", {CommandSyntaxChecker::COMMAND_ONLY, - [](const absl::string_view, absl::optional) { + [](const absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2157,7 +2244,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_ISSUER", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2166,7 +2253,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_ISSUER_FINGERPRINT_256", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2175,7 +2262,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_ISSUER_SERIAL", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2184,7 +2271,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_PEER_CERT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique< StreamInfoSslConnectionInfoFormatterProvider>( [](const Ssl::ConnectionInfo& connection_info) { @@ -2193,10 +2280,10 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_TRANSPORT_FAILURE_REASON", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (!stream_info.downstreamTransportFailureReason() .empty()) { result = absl::StrReplaceAll( @@ -2208,10 +2295,10 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"DOWNSTREAM_LOCAL_CLOSE_REASON", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (!stream_info.downstreamLocalCloseReason().empty()) { result = absl::StrReplaceAll( stream_info.downstreamLocalCloseReason(), @@ -2222,10 +2309,10 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_TRANSPORT_FAILURE_REASON", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (stream_info.upstreamInfo().has_value() && !stream_info.upstreamInfo() .value() @@ -2245,24 +2332,24 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_DETECTED_CLOSE_TYPE", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { if (stream_info.upstreamInfo().has_value()) { return detectedCloseTypeToString( stream_info.upstreamInfo() ->upstreamDetectedCloseType()); } - return absl::nullopt; + return std::nullopt; }); }}}, {"UPSTREAM_LOCAL_CLOSE_REASON", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (stream_info.upstreamInfo().has_value() && !stream_info.upstreamInfo() .value() @@ -2282,8 +2369,8 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"HOSTNAME", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { - absl::optional hostname = + [](absl::string_view, std::optional) { + std::optional hostname = SubstitutionFormatUtils::getHostname(); return std::make_unique( [hostname](const StreamInfo::StreamInfo&) { @@ -2292,10 +2379,10 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"FILTER_CHAIN_NAME", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { if (const auto info = stream_info.downstreamAddressProvider() .filterChainInfo(); info.has_value()) { @@ -2303,24 +2390,24 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return std::string(info->name()); } } - return absl::nullopt; + return std::nullopt; }); }}}, {"VIRTUAL_CLUSTER_NAME", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return stream_info.virtualClusterName(); }); }}}, {"TLS_JA3_FINGERPRINT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (!stream_info.downstreamAddressProvider() .ja3Hash() .empty()) { @@ -2332,10 +2419,10 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"TLS_JA4_FINGERPRINT", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) { - absl::optional result; + std::optional result; if (!stream_info.downstreamAddressProvider() .ja4Hash() .empty()) { @@ -2347,20 +2434,20 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UNIQUE_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, const absl::optional&) { + [](absl::string_view, const std::optional&) { return std::make_unique( [](const StreamInfo::StreamInfo&) - -> absl::optional { - return absl::make_optional( + -> std::optional { + return std::make_optional( Random::RandomUtility::uuid()); }); }}}, {"STREAM_ID", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, absl::optional) { + [](absl::string_view, std::optional) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { auto provider = stream_info.getStreamIdProvider(); if (!provider.has_value()) { return {}; @@ -2369,51 +2456,51 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide if (!id.has_value()) { return {}; } - return absl::make_optional(id.value()); + return std::make_optional(id.value()); }); }}}, {"START_TIME", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique( + [](absl::string_view format, std::optional) { + return SystemTimeFormatter::make( format, std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return stream_info.startTime(); })); }}}, {"START_TIME_LOCAL", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique( + [](absl::string_view format, std::optional) { + return SystemTimeFormatter::make( format, std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return stream_info.startTime(); }), true); }}}, {"EMIT_TIME", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique( + [](absl::string_view format, std::optional) { + return SystemTimeFormatter::make( format, std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return stream_info.timeSource().systemTime(); })); }}}, {"EMIT_TIME_LOCAL", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique( + [](absl::string_view format, std::optional) { + return SystemTimeFormatter::make( format, std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { return stream_info.timeSource().systemTime(); }), true); @@ -2421,7 +2508,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide {"DYNAMIC_METADATA", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { absl::string_view filter_namespace; std::vector path; @@ -2433,7 +2520,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide {"CLUSTER_METADATA", {CommandSyntaxChecker::PARAMS_REQUIRED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { absl::string_view filter_namespace; std::vector path; @@ -2444,7 +2531,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide }}}, {"UPSTREAM_METADATA", {CommandSyntaxChecker::PARAMS_REQUIRED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { absl::string_view filter_namespace; std::vector path; @@ -2456,47 +2543,48 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide {"FILTER_STATE", {CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { return FilterStateFormatter::create(format, max_length, false); }}}, {"UPSTREAM_FILTER_STATE", {CommandSyntaxChecker::PARAMS_OPTIONAL | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view format, absl::optional max_length) { + [](absl::string_view format, std::optional max_length) { return FilterStateFormatter::create(format, max_length, true); }}}, {"DOWNSTREAM_PEER_CERT_V_START", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique(format); + [](absl::string_view format, std::optional) { + return makeTimeFormatter( + format); }}}, {"DOWNSTREAM_PEER_CERT_V_END", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique(format); + [](absl::string_view format, std::optional) { + return makeTimeFormatter(format); }}}, {"UPSTREAM_PEER_CERT_V_START", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique(format); + [](absl::string_view format, std::optional) { + return makeTimeFormatter(format); }}}, {"UPSTREAM_PEER_CERT_V_END", {CommandSyntaxChecker::PARAMS_OPTIONAL, - [](absl::string_view format, absl::optional) { - return std::make_unique(format); + [](absl::string_view format, std::optional) { + return makeTimeFormatter(format); }}}, {"ENVIRONMENT", {CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED, - [](absl::string_view key, absl::optional max_length) { + [](absl::string_view key, std::optional max_length) { return std::make_unique(key, max_length); }}}, {"UPSTREAM_CONNECTION_POOL_READY_DURATION", {CommandSyntaxChecker::COMMAND_ONLY, - [](absl::string_view, const absl::optional&) { + [](absl::string_view, const std::optional&) { return std::make_unique( [](const StreamInfo::StreamInfo& stream_info) - -> absl::optional { + -> std::optional { if (auto upstream_info = stream_info.upstreamInfo(); upstream_info.has_value()) { if (auto connection_pool_callback_latency = @@ -2508,7 +2596,7 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return connection_pool_callback_latency; } } - return absl::nullopt; + return std::nullopt; }); }}}, }); @@ -2519,8 +2607,9 @@ class BuiltInStreamInfoCommandParser : public CommandParser { BuiltInStreamInfoCommandParser() = default; // CommandParser - FormatterProviderPtr parse(absl::string_view command, absl::string_view sub_command, - absl::optional max_length) const override { + absl::StatusOr parse(absl::string_view command, + absl::string_view sub_command, + std::optional max_length) const override { auto it = getKnownStreamInfoFormatterProviders().find(command); @@ -2533,7 +2622,10 @@ class BuiltInStreamInfoCommandParser : public CommandParser { THROW_IF_NOT_OK(Envoy::Formatter::CommandSyntaxChecker::verifySyntax( (*it).second.first, command, sub_command, max_length)); - return (*it).second.second(sub_command, max_length); + StreamInfoFormatterResult result = (*it).second.second(sub_command, max_length); + THROW_IF_NOT_OK_REF(result.status()); + + return (std::move(result)).value(); } }; diff --git a/source/common/formatter/stream_info_formatter.h b/source/common/formatter/stream_info_formatter.h index d7348ef423cc3..813122af67c07 100644 --- a/source/common/formatter/stream_info_formatter.h +++ b/source/common/formatter/stream_info_formatter.h @@ -3,10 +3,14 @@ #include #include #include +#include +#include #include #include +#include #include +#include "envoy/common/exception.h" #include "envoy/formatter/substitution_formatter.h" #include "envoy/stream_info/stream_info.h" @@ -14,7 +18,8 @@ #include "source/common/formatter/substitution_format_utility.h" #include "absl/container/flat_hash_map.h" -#include "absl/types/optional.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" namespace Envoy { namespace Formatter { @@ -22,8 +27,8 @@ namespace Formatter { class StreamInfoFormatterProvider : public FormatterProvider { public: // FormatterProvider - absl::optional format(const Context&, - const StreamInfo::StreamInfo& stream_info) const override { + std::optional format(const Context&, + const StreamInfo::StreamInfo& stream_info) const override { return format(stream_info); } Protobuf::Value formatValue(const Context&, @@ -34,10 +39,10 @@ class StreamInfoFormatterProvider : public FormatterProvider { /** * Format the value with the given stream info. * @param stream_info supplies the stream info. - * @return absl::optional optional string containing a single value extracted from + * @return std::optional optional string containing a single value extracted from * the given stream info. */ - virtual absl::optional format(const StreamInfo::StreamInfo& stream_info) const PURE; + virtual std::optional format(const StreamInfo::StreamInfo& stream_info) const PURE; /** * Format the value with the given stream info. @@ -48,9 +53,10 @@ class StreamInfoFormatterProvider : public FormatterProvider { }; using StreamInfoFormatterProviderPtr = std::unique_ptr; +using StreamInfoFormatterResult = absl::StatusOr; using StreamInfoFormatterProviderCreateFunc = - std::function)>; + std::function)>; enum class DurationPrecision { Milliseconds, Microseconds, Nanoseconds }; @@ -64,24 +70,24 @@ class MetadataFormatter : public StreamInfoFormatterProvider { using GetMetadataFunction = std::function; MetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length, GetMetadataFunction get); + std::optional max_length, GetMetadataFunction get); // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo& stream_info) const override; + std::optional format(const StreamInfo::StreamInfo& stream_info) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo& stream_info) const override; protected: - absl::optional + std::optional formatMetadata(const envoy::config::core::v3::Metadata& metadata) const; Protobuf::Value formatMetadataValue(const envoy::config::core::v3::Metadata& metadata) const; private: std::string filter_namespace_; std::vector path_; - absl::optional max_length_; + std::optional max_length_; GetMetadataFunction get_func_; }; @@ -92,7 +98,7 @@ class DynamicMetadataFormatter : public MetadataFormatter { public: DynamicMetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length); + std::optional max_length); }; /** @@ -102,7 +108,7 @@ class ClusterMetadataFormatter : public MetadataFormatter { public: ClusterMetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length); + std::optional max_length); }; /** @@ -112,7 +118,7 @@ class UpstreamHostMetadataFormatter : public MetadataFormatter { public: UpstreamHostMetadataFormatter(absl::string_view filter_namespace, const std::vector& path, - absl::optional max_length); + std::optional max_length); }; enum class FilterStateFormat { String, Proto, Field }; @@ -122,26 +128,30 @@ enum class FilterStateFormat { String, Proto, Field }; */ class FilterStateFormatter : public StreamInfoFormatterProvider { public: - static std::unique_ptr - create(absl::string_view format, absl::optional max_length, bool is_upstream); + static absl::StatusOr> + create(absl::string_view format, std::optional max_length, bool is_upstream); - FilterStateFormatter(absl::string_view key, absl::optional max_length, - bool serialize_as_string, bool is_upstream = false, - absl::string_view field_name = {}); + static absl::StatusOr> + createForTest(absl::string_view key, std::optional max_length, bool serialize_as_string, + bool is_upstream = false, absl::string_view field_name = {}); // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo&) const override; + std::optional format(const StreamInfo::StreamInfo&) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; private: + FilterStateFormatter(absl::string_view key, std::optional max_length, + bool serialize_as_string, bool is_upstream = false, + absl::string_view field_name = {}); + const Envoy::StreamInfo::FilterState::Object* filterState(const StreamInfo::StreamInfo& stream_info) const; std::string key_; - absl::optional max_length_; + std::optional max_length_; const bool is_upstream_; FilterStateFormat format_; @@ -151,26 +161,27 @@ class FilterStateFormatter : public StreamInfoFormatterProvider { class CommonDurationFormatter : public StreamInfoFormatterProvider { public: using TimePointGetter = - std::function(const StreamInfo::StreamInfo&)>; + std::function(const StreamInfo::StreamInfo&)>; - static std::unique_ptr create(absl::string_view sub_command); - - CommonDurationFormatter(TimePointGetter beg, TimePointGetter end, - DurationPrecision duration_precision) - : time_point_beg_(std::move(beg)), time_point_end_(std::move(end)), - duration_precision_(duration_precision) {} + static absl::StatusOr> + create(absl::string_view sub_command); // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo&) const override; + std::optional format(const StreamInfo::StreamInfo&) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; static const absl::flat_hash_map KnownTimePointGetters; private: - absl::optional getDurationCount(const StreamInfo::StreamInfo& info) const; + CommonDurationFormatter(TimePointGetter beg, TimePointGetter end, + DurationPrecision duration_precision) + : time_point_beg_(std::move(beg)), time_point_end_(std::move(end)), + duration_precision_(duration_precision) {} + + std::optional getDurationCount(const StreamInfo::StreamInfo& info) const; static TimePointGetter getTimePointGetterByName(absl::string_view name); @@ -182,6 +193,10 @@ class CommonDurationFormatter : public StreamInfoFormatterProvider { "DS_RX_BEG"; // Downstream request receiving begin. static constexpr absl::string_view LastDownstreamRxByteReceived = "DS_RX_END"; // Downstream request receiving end. + static constexpr absl::string_view DownstreamConnectionBegin = + "DS_CX_BEG"; // Downstream connection begin. + static constexpr absl::string_view DownstreamConnectionEnd = + "DS_CX_END"; // Downstream connection end. static constexpr absl::string_view UpstreamConnectStart = "US_CX_BEG"; // Upstream TCP connection establishment start. static constexpr absl::string_view UpstreamConnectEnd = @@ -214,18 +229,27 @@ class CommonDurationFormatter : public StreamInfoFormatterProvider { class SystemTimeFormatter : public StreamInfoFormatterProvider { public: using TimeFieldExtractor = - std::function(const StreamInfo::StreamInfo& stream_info)>; + std::function(const StreamInfo::StreamInfo& stream_info)>; using TimeFieldExtractorPtr = std::unique_ptr; - SystemTimeFormatter(absl::string_view format, TimeFieldExtractorPtr f, bool local_time = false); + static absl::StatusOr> + make(absl::string_view format, TimeFieldExtractorPtr&& f, bool local_time = false) { + RETURN_IF_NOT_OK(checkConstructPreconditions(format)); + return std::unique_ptr( + new SystemTimeFormatter(format, std::move(f), local_time)); + } // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo&) const override; + std::optional format(const StreamInfo::StreamInfo&) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; +protected: + SystemTimeFormatter(absl::string_view format, TimeFieldExtractorPtr f, bool local_time = false); + static absl::Status checkConstructPreconditions(absl::string_view format); + private: const Envoy::DateFormatter date_formatter_; const TimeFieldExtractorPtr time_field_extractor_; @@ -237,8 +261,11 @@ class SystemTimeFormatter : public StreamInfoFormatterProvider { * SystemTimeFormatter (FormatterProvider) for request start time from StreamInfo. */ class StartTimeFormatter : public SystemTimeFormatter { -public: +protected: StartTimeFormatter(absl::string_view format); + + template + friend absl::StatusOr> makeTimeFormatter(absl::string_view format); }; /** @@ -246,8 +273,11 @@ class StartTimeFormatter : public SystemTimeFormatter { * ConnectionInfo. */ class DownstreamPeerCertVStartFormatter : public SystemTimeFormatter { -public: +protected: DownstreamPeerCertVStartFormatter(absl::string_view format); + + template + friend absl::StatusOr> makeTimeFormatter(absl::string_view format); }; /** @@ -255,8 +285,11 @@ class DownstreamPeerCertVStartFormatter : public SystemTimeFormatter { * ConnectionInfo. */ class DownstreamPeerCertVEndFormatter : public SystemTimeFormatter { -public: +protected: DownstreamPeerCertVEndFormatter(absl::string_view format); + + template + friend absl::StatusOr> makeTimeFormatter(absl::string_view format); }; /** @@ -264,8 +297,11 @@ class DownstreamPeerCertVEndFormatter : public SystemTimeFormatter { * upstreamInfo. */ class UpstreamPeerCertVStartFormatter : public SystemTimeFormatter { -public: +protected: UpstreamPeerCertVStartFormatter(absl::string_view format); + + template + friend absl::StatusOr> makeTimeFormatter(absl::string_view format); }; /** @@ -273,22 +309,39 @@ class UpstreamPeerCertVStartFormatter : public SystemTimeFormatter { * upstreamInfo. */ class UpstreamPeerCertVEndFormatter : public SystemTimeFormatter { -public: +protected: UpstreamPeerCertVEndFormatter(absl::string_view format); + + template + friend absl::StatusOr> makeTimeFormatter(absl::string_view format); }; +/** + * Factory method for creating an object of type derived from SystemTimeFormatter + * The method first checks constructor preconditions are satisfied. If not the method + * return an error. + * Otherwise it returns unique_ptr with an object. + */ +template +absl::StatusOr> makeTimeFormatter(absl::string_view format) { + static_assert(std::is_base_of::value, + "T must be derived from SystemTimeFormatter"); + RETURN_IF_NOT_OK(T::checkConstructPreconditions(format)); + return std::unique_ptr(new T(format)); +} + /** * FormatterProvider for environment. If no valid environment value then */ class EnvironmentFormatter : public StreamInfoFormatterProvider { public: - EnvironmentFormatter(absl::string_view key, absl::optional max_length); + EnvironmentFormatter(absl::string_view key, std::optional max_length); // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo&) const override; + std::optional format(const StreamInfo::StreamInfo&) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; private: @@ -311,21 +364,24 @@ class RequestedServerNameFormatter : public StreamInfoFormatterProvider { OriginalHostOnly, }; - RequestedServerNameFormatter(absl::string_view fallback, absl::string_view option); + static absl::StatusOr> + create(absl::string_view source, absl::string_view option); // StreamInfoFormatterProvider // Don't hide the other structure of format and formatValue. using StreamInfoFormatterProvider::format; using StreamInfoFormatterProvider::formatValue; - absl::optional format(const StreamInfo::StreamInfo&) const override; + std::optional format(const StreamInfo::StreamInfo&) const override; Protobuf::Value formatValue(const StreamInfo::StreamInfo&) const override; - absl::optional getHostFromHeaders(const StreamInfo::StreamInfo& stream_info) const; - absl::optional getSNIFromStreamInfo(const StreamInfo::StreamInfo& stream_info) const; + std::optional getHostFromHeaders(const StreamInfo::StreamInfo& stream_info) const; + std::optional getSNIFromStreamInfo(const StreamInfo::StreamInfo& stream_info) const; private: - HostFormatterSource source_; - HostFormatterOption option_; + RequestedServerNameFormatter(HostFormatterSource source, HostFormatterOption option); + + const HostFormatterSource source_; + const HostFormatterOption option_; }; class DefaultBuiltInStreamInfoCommandParserFactory : public BuiltInCommandParserFactory { diff --git a/source/common/formatter/substitution_format_string.cc b/source/common/formatter/substitution_format_string.cc index 60cabc98f997d..57a66a590a7b2 100644 --- a/source/common/formatter/substitution_format_string.cc +++ b/source/common/formatter/substitution_format_string.cc @@ -1,5 +1,7 @@ #include "source/common/formatter/substitution_format_string.h" +#include "absl/status/statusor.h" + namespace Envoy { namespace Formatter { @@ -51,7 +53,7 @@ absl::StatusOr SubstitutionFormatStringUtils::fromProtoConfig( return nullptr; } -FormatterPtr +absl::StatusOr SubstitutionFormatStringUtils::createJsonFormatter(const Protobuf::Struct& struct_format, bool omit_empty_values, const std::vector& commands) { diff --git a/source/common/formatter/substitution_format_string.h b/source/common/formatter/substitution_format_string.h index 686f54542ad39..7dd840d3de7d1 100644 --- a/source/common/formatter/substitution_format_string.h +++ b/source/common/formatter/substitution_format_string.h @@ -15,6 +15,8 @@ #include "source/common/runtime/runtime_features.h" #include "source/server/generic_factory_context.h" +#include "absl/status/statusor.h" + namespace Envoy { namespace Formatter { @@ -44,9 +46,9 @@ class SubstitutionFormatStringUtils { /** * Generate a Json formatter object from proto::Struct config */ - static FormatterPtr createJsonFormatter(const Protobuf::Struct& struct_format, - bool omit_empty_values, - const std::vector& commands = {}); + static absl::StatusOr + createJsonFormatter(const Protobuf::Struct& struct_format, bool omit_empty_values, + const std::vector& commands = {}); }; } // namespace Formatter diff --git a/source/common/formatter/substitution_format_utility.cc b/source/common/formatter/substitution_format_utility.cc index 5c7f44a8dbc5d..0bff3efa36d01 100644 --- a/source/common/formatter/substitution_format_utility.cc +++ b/source/common/formatter/substitution_format_utility.cc @@ -17,13 +17,13 @@ static const std::string DefaultUnspecifiedValueString = "-"; absl::Status CommandSyntaxChecker::verifySyntax(CommandSyntaxChecker::CommandSyntaxFlags flags, absl::string_view command, absl::string_view subcommand, - absl::optional length) { - if ((flags == COMMAND_ONLY) && ((subcommand.length() != 0) || length.has_value())) { + std::optional length) { + if ((flags == COMMAND_ONLY) && ((!subcommand.empty()) || length.has_value())) { return absl::InvalidArgumentError( fmt::format("{} does not take any parameters or length", command)); } - if ((flags & PARAMS_REQUIRED).any() && (subcommand.length() == 0)) { + if ((flags & PARAMS_REQUIRED).any() && (subcommand.empty())) { return absl::InvalidArgumentError(fmt::format("{} requires parameters", command)); } @@ -34,23 +34,23 @@ absl::Status CommandSyntaxChecker::verifySyntax(CommandSyntaxChecker::CommandSyn return absl::OkStatus(); } -const absl::optional> -SubstitutionFormatUtils::protocolToString(const absl::optional& protocol) { +const std::optional> +SubstitutionFormatUtils::protocolToString(const std::optional& protocol) { if (protocol) { return Http::Utility::getProtocolString(protocol.value()); } - return absl::nullopt; + return std::nullopt; } const std::string& -SubstitutionFormatUtils::protocolToStringOrDefault(const absl::optional& protocol) { +SubstitutionFormatUtils::protocolToStringOrDefault(const std::optional& protocol) { if (protocol) { return Http::Utility::getProtocolString(protocol.value()); } return DefaultUnspecifiedValueString; } -const absl::optional SubstitutionFormatUtils::getHostname() { +const std::optional SubstitutionFormatUtils::getHostname() { #ifdef HOST_NAME_MAX const size_t len = HOST_NAME_MAX; #else @@ -61,7 +61,7 @@ const absl::optional SubstitutionFormatUtils::getHostname() { Api::OsSysCalls& os_sys_calls = Api::OsSysCallsSingleton::get(); const Api::SysCallIntResult result = os_sys_calls.gethostname(name, len); - absl::optional hostname; + std::optional hostname; if (result.return_value_ == 0) { hostname = name; } @@ -73,7 +73,7 @@ const Protobuf::Value& SubstitutionFormatUtils::unspecifiedValue() { return ValueUtil::nullValue(); } -bool SubstitutionFormatUtils::truncate(std::string& str, absl::optional max_length) { +bool SubstitutionFormatUtils::truncate(std::string& str, std::optional max_length) { if (!max_length) { return false; } @@ -87,7 +87,7 @@ bool SubstitutionFormatUtils::truncate(std::string& str, absl::optional } absl::string_view SubstitutionFormatUtils::truncateStringView(absl::string_view str, - absl::optional max_length) { + std::optional max_length) { if (!max_length) { return str; } diff --git a/source/common/formatter/substitution_format_utility.h b/source/common/formatter/substitution_format_utility.h index 70660ecf4479e..4a6fcf85de743 100644 --- a/source/common/formatter/substitution_format_utility.h +++ b/source/common/formatter/substitution_format_utility.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -9,7 +10,6 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" -#include "absl/types/optional.h" #include "fmt/format.h" namespace Envoy { @@ -25,7 +25,7 @@ class CommandSyntaxChecker { static absl::Status verifySyntax(CommandSyntaxChecker::CommandSyntaxFlags flags, absl::string_view command, absl::string_view subcommand, - absl::optional length); + std::optional length); }; /** @@ -35,11 +35,11 @@ class SubstitutionFormatUtils { public: // Optional references are not supported, but this method has large performance // impact, so using reference_wrapper. - static const absl::optional> - protocolToString(const absl::optional& protocol); + static const std::optional> + protocolToString(const std::optional& protocol); static const std::string& - protocolToStringOrDefault(const absl::optional& protocol); - static const absl::optional getHostname(); + protocolToStringOrDefault(const std::optional& protocol); + static const std::optional getHostname(); /** * Unspecified value for protobuf. @@ -51,7 +51,7 @@ class SubstitutionFormatUtils { * max_length is greater than the length of the string. * @return true if the string was truncated, false otherwise. */ - static bool truncate(std::string& str, absl::optional max_length); + static bool truncate(std::string& str, std::optional max_length); /** * Truncate an input string view to a maximum length, and return the resulting string view. Do not @@ -59,7 +59,7 @@ class SubstitutionFormatUtils { * view. */ static absl::string_view truncateStringView(absl::string_view str, - absl::optional max_length); + std::optional max_length); /** * Parse a header subcommand of the form: X?Y . diff --git a/source/common/formatter/substitution_formatter.cc b/source/common/formatter/substitution_formatter.cc index fbc8c90215786..d7383e904bbc7 100644 --- a/source/common/formatter/substitution_formatter.cc +++ b/source/common/formatter/substitution_formatter.cc @@ -1,5 +1,7 @@ #include "source/common/formatter/substitution_formatter.h" +#include "source/common/formatter/builtin_command_parser_factory_helper.h" + namespace Envoy { namespace Formatter { @@ -304,7 +306,7 @@ SubstitutionFormatParser::parse(absl::string_view format, const size_t sub_format_size = sub_format.size(); absl::string_view command, command_arg; - absl::optional max_len; + std::optional max_len; if (!re2::RE2::Consume(&sub_format, commandWithArgsRegex(), &command, &command_arg, &max_len)) { return absl::InvalidArgumentError(fmt::format( @@ -316,7 +318,10 @@ SubstitutionFormatParser::parse(absl::string_view format, // First try the command parsers provided by the user. This allows the user to override // built-in command parsers. for (const auto& cmd : command_parsers) { - auto formatter = cmd->parse(command, command_arg, max_len); + absl::StatusOr formatter_result = + cmd->parse(command, command_arg, max_len); + RETURN_IF_ERROR(formatter_result.status()); + FormatterProviderPtr formatter = std::move(formatter_result).value(); if (formatter) { formatters.push_back(std::move(formatter)); added = true; @@ -327,7 +332,10 @@ SubstitutionFormatParser::parse(absl::string_view format, // Next, try the built-in command parsers. if (!added) { for (const auto& cmd : BuiltInCommandParserFactoryHelper::commandParsers()) { - auto formatter = cmd->parse(command, command_arg, max_len); + absl::StatusOr formatter_result = + cmd->parse(command, command_arg, max_len); + RETURN_IF_ERROR(formatter_result.status()); + FormatterProviderPtr formatter = std::move(formatter_result).value(); if (formatter) { formatters.push_back(std::move(formatter)); added = true; @@ -369,7 +377,7 @@ std::string FormatterImpl::format(const Context& context, log_line.reserve(256); for (const auto& provider : providers_) { - const absl::optional bit = provider->format(context, stream_info); + const std::optional bit = provider->format(context, stream_info); // Add the formatted value if there is one. Otherwise add a default value // of "-" if omit_empty_values_ is not set. if (bit.has_value()) { @@ -387,7 +395,7 @@ void stringValueToLogLine(const JsonFormatterImpl::Formatters& formatters, const std::string& sanitize, bool omit_empty_values) { log_line.push_back('"'); // Start the JSON string. for (const JsonFormatterImpl::Formatter& formatter : formatters) { - const absl::optional value = formatter->format(context, info); + const std::optional value = formatter->format(context, info); if (!value.has_value()) { // Add the empty value. This needn't be sanitized. log_line.append(omit_empty_values ? EMPTY_STRING : DefaultUnspecifiedValueStringView); diff --git a/source/common/formatter/substitution_formatter.h b/source/common/formatter/substitution_formatter.h index e788508351c43..e608bd3b844ce 100644 --- a/source/common/formatter/substitution_formatter.h +++ b/source/common/formatter/substitution_formatter.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -18,7 +19,6 @@ #include "source/common/json/json_streamer.h" #include "source/common/json/json_utility.h" -#include "absl/types/optional.h" #include "re2/re2.h" namespace Envoy { @@ -33,7 +33,7 @@ class PlainStringFormatter : public FormatterProvider { PlainStringFormatter(absl::string_view str) { str_.set_string_value(str); } // FormatterProvider - absl::optional format(const Context&, const StreamInfo::StreamInfo&) const override { + std::optional format(const Context&, const StreamInfo::StreamInfo&) const override { return str_.string_value(); } Protobuf::Value formatValue(const Context&, const StreamInfo::StreamInfo&) const override { @@ -52,7 +52,7 @@ class PlainNumberFormatter : public FormatterProvider { PlainNumberFormatter(double num) { num_.set_number_value(num); } // FormatterProvider - absl::optional format(const Context&, const StreamInfo::StreamInfo&) const override { + std::optional format(const Context&, const StreamInfo::StreamInfo&) const override { std::string str = absl::StrFormat("%g", num_.number_value()); return str; } diff --git a/source/common/grpc/BUILD b/source/common/grpc/BUILD index 9999fbb2c123b..e948de2ea9911 100644 --- a/source/common/grpc/BUILD +++ b/source/common/grpc/BUILD @@ -77,7 +77,6 @@ envoy_cc_library( hdrs = ["status.h"], deps = [ "//envoy/grpc:status", - "@abseil-cpp//absl/types:optional", ], ) @@ -108,7 +107,6 @@ envoy_cc_library( "//source/common/http:message_lib", "//source/common/http:utility_lib", "//source/common/protobuf", - "@abseil-cpp//absl/types:optional", ], ) @@ -125,7 +123,6 @@ envoy_cc_library( "//source/common/common:hash_lib", "//source/common/stats:symbol_table_lib", "//source/common/stats:utility_lib", - "@abseil-cpp//absl/types:optional", ], ) @@ -145,7 +142,6 @@ envoy_cc_library( "//source/common/common:macros", "//source/common/common:utility_lib", "//source/common/grpc:status_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) diff --git a/source/common/grpc/async_client_impl.cc b/source/common/grpc/async_client_impl.cc index ee772861b1c8d..5518994cbf4b8 100644 --- a/source/common/grpc/async_client_impl.cc +++ b/source/common/grpc/async_client_impl.cc @@ -284,6 +284,10 @@ void AsyncStreamImpl::onData(Buffer::Instance& data, bool end_stream) { streamError(Status::WellKnownGrpcStatus::Internal); return; } + // If the HTTP stream has already been reset, we can return early. + if (http_reset_) { + return; + } } if (end_stream) { diff --git a/source/common/grpc/async_client_impl.h b/source/common/grpc/async_client_impl.h index 2ad658321d762..4b6043cf5799d 100644 --- a/source/common/grpc/async_client_impl.h +++ b/source/common/grpc/async_client_impl.h @@ -114,7 +114,7 @@ class AsyncStreamImpl : public RawAsyncStream, void streamError(Status::GrpcStatus grpc_status) { streamError(grpc_status, EMPTY_STRING); } void cleanup(); - void trailerResponse(absl::optional grpc_status, + void trailerResponse(std::optional grpc_status, const std::string& grpc_message); // Deliver notification and update span when the connection closes. diff --git a/source/common/grpc/buffered_async_client.h b/source/common/grpc/buffered_async_client.h index d6b88b652b16e..85d46a9384eeb 100644 --- a/source/common/grpc/buffered_async_client.h +++ b/source/common/grpc/buffered_async_client.h @@ -35,11 +35,11 @@ template class BufferedAsyncClient { } // It push message into internal message buffer. - // If the buffer is full, it will return absl::nullopt. - absl::optional bufferMessage(RequestType& message) { + // If the buffer is full, it will return std::nullopt. + std::optional bufferMessage(RequestType& message) { const auto buffer_size = message.ByteSizeLong(); if (current_buffer_bytes_ + buffer_size > max_buffer_bytes_) { - return absl::nullopt; + return std::nullopt; } auto id = publishId(); diff --git a/source/common/grpc/common.cc b/source/common/grpc/common.cc index 43709bdc69761..b6af64ef7faaa 100644 --- a/source/common/grpc/common.cc +++ b/source/common/grpc/common.cc @@ -99,13 +99,13 @@ bool Common::isConnectStreamingResponseHeaders(const Http::ResponseHeaderMap& he return hasConnectStreamingContentType(headers); } -absl::optional +std::optional Common::getGrpcStatus(const Http::ResponseHeaderOrTrailerMap& trailers, bool allow_user_defined) { const absl::string_view grpc_status_header = trailers.getGrpcStatusValue(); uint64_t grpc_status_code; if (grpc_status_header.empty()) { - return absl::nullopt; + return std::nullopt; } if (!absl::SimpleAtoi(grpc_status_header, &grpc_status_code) || (grpc_status_code > Status::WellKnownGrpcStatus::MaximumKnown && !allow_user_defined)) { @@ -114,10 +114,10 @@ Common::getGrpcStatus(const Http::ResponseHeaderOrTrailerMap& trailers, bool all return {static_cast(grpc_status_code)}; } -absl::optional Common::getGrpcStatus(const Http::ResponseTrailerMap& trailers, - const Http::ResponseHeaderMap& headers, - const StreamInfo::StreamInfo& info, - bool allow_user_defined) { +std::optional Common::getGrpcStatus(const Http::ResponseTrailerMap& trailers, + const Http::ResponseHeaderMap& headers, + const StreamInfo::StreamInfo& info, + bool allow_user_defined) { // The gRPC specification does not guarantee a gRPC status code will be returned from a gRPC // request. When it is returned, it will be in the response trailers. With that said, Envoy will // treat a trailers-only response as a headers-only response, so we have to check the following @@ -125,7 +125,7 @@ absl::optional Common::getGrpcStatus(const Http::ResponseTra // 1. trailers gRPC status, if it exists. // 2. headers gRPC status, if it exists. // 3. Inferred from info HTTP status, if it exists. - absl::optional optional_status; + std::optional optional_status; optional_status = Grpc::Common::getGrpcStatus(trailers, allow_user_defined); if (optional_status.has_value()) { return optional_status; @@ -134,9 +134,9 @@ absl::optional Common::getGrpcStatus(const Http::ResponseTra if (optional_status.has_value()) { return optional_status; } - return info.responseCode() ? absl::optional( + return info.responseCode() ? std::optional( Grpc::Utility::httpToGrpcStatus(info.responseCode().value())) - : absl::nullopt; + : std::nullopt; } std::string Common::getGrpcMessage(const Http::ResponseHeaderOrTrailerMap& trailers) { @@ -144,23 +144,23 @@ std::string Common::getGrpcMessage(const Http::ResponseHeaderOrTrailerMap& trail return entry ? std::string(entry->value().getStringView()) : EMPTY_STRING; } -absl::optional +std::optional Common::getGrpcStatusDetailsBin(const Http::HeaderMap& trailers) { const auto details_header = trailers.get(Http::Headers::get().GrpcStatusDetailsBin); if (details_header.empty()) { - return absl::nullopt; + return std::nullopt; } // Some implementations use non-padded base64 encoding for grpc-status-details-bin. // This is effectively a trusted header so using the first value is fine. auto decoded_value = Base64::decodeWithoutPadding(details_header[0]->value().getStringView()); if (decoded_value.empty()) { - return absl::nullopt; + return std::nullopt; } google::rpc::Status status; if (!status.ParseFromString(decoded_value)) { - return absl::nullopt; + return std::nullopt; } return {std::move(status)}; @@ -201,7 +201,7 @@ Buffer::InstancePtr Common::serializeMessage(const Protobuf::Message& message) { return body; } -absl::optional +std::optional Common::getGrpcTimeout(const Http::RequestHeaderMap& request_headers) { const Http::HeaderEntry* header_grpc_timeout_entry = request_headers.GrpcTimeout(); std::chrono::milliseconds timeout; @@ -211,7 +211,7 @@ Common::getGrpcTimeout(const Http::RequestHeaderMap& request_headers) { if (timeout_entry.empty()) { // Must be of the form TimeoutValue TimeoutUnit. See // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests. - return absl::nullopt; + return std::nullopt; } // TimeoutValue must be a positive integer of at most 8 digits. if (absl::SimpleAtoi(timeout_entry.substr(0, timeout_entry.size() - 1), &grpc_timeout) && @@ -244,7 +244,7 @@ Common::getGrpcTimeout(const Http::RequestHeaderMap& request_headers) { } } } - return absl::nullopt; + return std::nullopt; } void Common::toGrpcTimeout(const std::chrono::milliseconds& timeout, @@ -270,7 +270,7 @@ void Common::toGrpcTimeout(const std::chrono::milliseconds& timeout, Http::RequestMessagePtr Common::prepareHeaders(absl::string_view host_name, absl::string_view service_full_name, absl::string_view method_name, - const absl::optional& timeout) { + const std::optional& timeout) { Http::RequestMessagePtr message(new Http::RequestMessageImpl()); message->headers().setReferenceMethod(Http::Headers::get().MethodValues.Post); message->headers().setPath(absl::StrCat("/", service_full_name, "/", method_name)); @@ -307,9 +307,8 @@ bool Common::parseBufferInstance(Buffer::InstancePtr&& buffer, Protobuf::Message return proto.ParseFromZeroCopyStream(&stream); } -absl::optional -Common::resolveServiceAndMethod(const Http::HeaderEntry* path) { - absl::optional request_names; +std::optional Common::resolveServiceAndMethod(const Http::HeaderEntry* path) { + std::optional request_names; if (path == nullptr) { return request_names; } diff --git a/source/common/grpc/common.h b/source/common/grpc/common.h index 096d103ba94fe..5bbc651d24c4f 100644 --- a/source/common/grpc/common.h +++ b/source/common/grpc/common.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/exception.h" @@ -14,7 +15,6 @@ #include "source/common/grpc/status.h" #include "source/common/protobuf/protobuf.h" -#include "absl/types/optional.h" #include "google/rpc/status.pb.h" namespace Envoy { @@ -22,10 +22,10 @@ namespace Grpc { class Exception : public EnvoyException { public: - Exception(const absl::optional& grpc_status, const std::string& message) + Exception(const std::optional& grpc_status, const std::string& message) : EnvoyException(message), grpc_status_(grpc_status) {} - const absl::optional grpc_status_; + const std::optional grpc_status_; }; class Common { @@ -102,10 +102,10 @@ class Common { * @param trailers the trailers to parse. * @param allow_user_status whether allow user defined grpc status. * if this value is false, custom grpc status is regarded as invalid status - * @return absl::optional the parsed status code or InvalidCode if no valid + * @return std::optional the parsed status code or InvalidCode if no valid * status is found. */ - static absl::optional + static std::optional getGrpcStatus(const Http::ResponseHeaderOrTrailerMap& trailers, bool allow_user_defined = false); /** @@ -114,13 +114,13 @@ class Common { * @param headers the headers to parse if no status code was found in the trailers * @param info the StreamInfo to check for HTTP response code if no code was found in the trailers * or headers - * @return absl::optional the parsed status code or absl::nullopt if no status + * @return std::optional the parsed status code or std::nullopt if no status * is found */ - static absl::optional getGrpcStatus(const Http::ResponseTrailerMap& trailers, - const Http::ResponseHeaderMap& headers, - const StreamInfo::StreamInfo& info, - bool allow_user_defined = false); + static std::optional getGrpcStatus(const Http::ResponseTrailerMap& trailers, + const Http::ResponseHeaderMap& headers, + const StreamInfo::StreamInfo& info, + bool allow_user_defined = false); /** * Returns the grpc-message from a given set of trailers, if present. @@ -136,7 +136,7 @@ class Common { * @return std::unique_ptr the gRPC status message or empty pointer if no * grpc-status-details-bin trailer found or it was invalid. */ - static absl::optional + static std::optional getGrpcStatusDetailsBin(const Http::HeaderMap& trailers); /** @@ -144,10 +144,10 @@ class Common { * @param request_headers the header map from which to extract the value of 'grpc-timeout' header. * If this header is missing the timeout corresponds to infinity. The header is encoded in * maximum of 8 decimal digits and a char for the unit. - * @return absl::optional the duration in milliseconds. absl::nullopt + * @return std::optional the duration in milliseconds. std::nullopt * is returned if 'grpc-timeout' is missing or malformed. */ - static absl::optional + static std::optional getGrpcTimeout(const Http::RequestHeaderMap& request_headers); /** @@ -175,7 +175,7 @@ class Common { static Http::RequestMessagePtr prepareHeaders(absl::string_view upstream_cluster, absl::string_view service_full_name, absl::string_view method_name, - const absl::optional& timeout); + const std::optional& timeout); /** * @return const std::string& type URL prefix. @@ -215,7 +215,7 @@ class Common { * a populated RequestNames, otherwise returns an empty optional. * @note The return value is only valid as long as `path` is still valid and unmodified. */ - static absl::optional resolveServiceAndMethod(const Http::HeaderEntry* path); + static std::optional resolveServiceAndMethod(const Http::HeaderEntry* path); private: static constexpr size_t MAX_GRPC_TIMEOUT_VALUE = 99999999; diff --git a/source/common/grpc/context_impl.cc b/source/common/grpc/context_impl.cc index 9d5ffe0541350..98dc8a23c4ff8 100644 --- a/source/common/grpc/context_impl.cc +++ b/source/common/grpc/context_impl.cc @@ -25,7 +25,7 @@ ContextImpl::ContextImpl(Stats::SymbolTable& symbol_table) // Gets the stat prefix and underlying storage, depending on whether request_names is empty Stats::ElementVec ContextImpl::statElements(Protocol protocol, - const absl::optional& request_names, + const std::optional& request_names, Stats::Element suffix) { const Stats::StatName protocolName = protocolStatName(protocol); if (request_names) { @@ -35,7 +35,7 @@ Stats::ElementVec ContextImpl::statElements(Protocol protocol, } void ContextImpl::chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, + const std::optional& request_names, const Http::HeaderEntry* grpc_status) { if (!grpc_status) { return; @@ -52,7 +52,7 @@ void ContextImpl::chargeStat(const Upstream::ClusterInfo& cluster, Protocol prot } void ContextImpl::chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, bool success) { + const std::optional& request_names, bool success) { Stats::ElementVec elements = statElements(protocol, request_names, successStatName(success)); Stats::Utility::counterFromElements(cluster.statsScope(), elements).inc(); elements.back() = total_; @@ -60,26 +60,26 @@ void ContextImpl::chargeStat(const Upstream::ClusterInfo& cluster, Protocol prot } void ContextImpl::chargeStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, bool success) { + const std::optional& request_names, bool success) { chargeStat(cluster, Protocol::Grpc, request_names, success); } void ContextImpl::chargeRequestMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) { Stats::ElementVec elements = statElements(Protocol::Grpc, request_names, request_message_count_); Stats::Utility::counterFromElements(cluster.statsScope(), elements).add(amount); } void ContextImpl::chargeResponseMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) { Stats::ElementVec elements = statElements(Protocol::Grpc, request_names, response_message_count_); Stats::Utility::counterFromElements(cluster.statsScope(), elements).add(amount); } void ContextImpl::chargeUpstreamStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, std::chrono::milliseconds duration) { Stats::ElementVec elements = statElements(Protocol::Grpc, request_names, upstream_rq_time_); Stats::Utility::histogramFromElements(cluster.statsScope(), elements, @@ -87,9 +87,9 @@ void ContextImpl::chargeUpstreamStat(const Upstream::ClusterInfo& cluster, .recordValue(duration.count()); } -absl::optional +std::optional ContextImpl::resolveDynamicServiceAndMethod(const Http::HeaderEntry* path) { - absl::optional request_names = Common::resolveServiceAndMethod(path); + std::optional request_names = Common::resolveServiceAndMethod(path); if (!request_names) { return {}; @@ -103,9 +103,9 @@ ContextImpl::resolveDynamicServiceAndMethod(const Http::HeaderEntry* path) { return RequestStatNames{service, method}; } -absl::optional +std::optional ContextImpl::resolveDynamicServiceAndMethodWithDotReplaced(const Http::HeaderEntry* path) { - absl::optional request_names = Common::resolveServiceAndMethod(path); + std::optional request_names = Common::resolveServiceAndMethod(path); if (!request_names) { return {}; } diff --git a/source/common/grpc/context_impl.h b/source/common/grpc/context_impl.h index c56ad4f9a8cfa..99ae984fc2fd1 100644 --- a/source/common/grpc/context_impl.h +++ b/source/common/grpc/context_impl.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/grpc/context.h" @@ -12,8 +13,6 @@ #include "source/common/stats/symbol_table.h" #include "source/common/stats/utility.h" -#include "absl/types/optional.h" - namespace Envoy { namespace Grpc { @@ -28,20 +27,20 @@ class ContextImpl : public Context { // Context void chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, + const std::optional& request_names, const Http::HeaderEntry* grpc_status) override; void chargeStat(const Upstream::ClusterInfo& cluster, Protocol protocol, - const absl::optional& request_names, bool success) override; + const std::optional& request_names, bool success) override; void chargeStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, bool success) override; + const std::optional& request_names, bool success) override; void chargeRequestMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) override; void chargeResponseMessageStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, uint64_t amount) override; void chargeUpstreamStat(const Upstream::ClusterInfo& cluster, - const absl::optional& request_names, + const std::optional& request_names, std::chrono::milliseconds duration) override; /** @@ -50,7 +49,7 @@ class ContextImpl : public Context { * @return if both gRPC serve and method have been resolved successfully returns * a populated RequestStatNames, otherwise returns an empty optional. */ - absl::optional + std::optional resolveDynamicServiceAndMethod(const Http::HeaderEntry* path) override; /** @@ -60,7 +59,7 @@ class ContextImpl : public Context { * @return if both gRPC serve and method have been resolved successfully returns * a populated RequestStatNames, otherwise returns an empty optional. */ - absl::optional + std::optional resolveDynamicServiceAndMethodWithDotReplaced(const Http::HeaderEntry* path) override; Stats::StatName successStatName(bool success) const { return success ? success_ : failure_; } @@ -74,7 +73,7 @@ class ContextImpl : public Context { // Creates an array of stat-name elements, comprising the protocol, optional // service and method, and a suffix. Stats::ElementVec statElements(Protocol protocol, - const absl::optional& request_names, + const std::optional& request_names, Stats::Element suffix); Stats::StatNamePool stat_name_pool_; diff --git a/source/common/grpc/google_async_client_impl.h b/source/common/grpc/google_async_client_impl.h index 5c5d13ad29bff..4db8cc9f5104e 100644 --- a/source/common/grpc/google_async_client_impl.h +++ b/source/common/grpc/google_async_client_impl.h @@ -274,7 +274,7 @@ class GoogleAsyncStreamImpl : public RawAsyncStream, // End-of-stream with no additional message. PendingMessage() = default; - const absl::optional buf_; + const std::optional buf_; const bool end_stream_{true}; }; diff --git a/source/common/grpc/typed_async_client.h b/source/common/grpc/typed_async_client.h index e664106c04688..e988410a21ef1 100644 --- a/source/common/grpc/typed_async_client.h +++ b/source/common/grpc/typed_async_client.h @@ -77,7 +77,7 @@ template class AsyncRequestCallbacks : public RawAsyncReques private: void onSuccessRaw(Buffer::InstancePtr&& response, Tracing::Span& span) override { - auto message = ResponsePtr(dynamic_cast( + auto message = ResponsePtr(Envoy::Protobuf::DynamicCastMessage( Internal::parseMessageUntyped(std::make_unique(), std::move(response)) .release())); if (!message) { @@ -98,7 +98,7 @@ template class AsyncStreamCallbacks : public RawAsyncStreamC private: bool onReceiveMessageRaw(Buffer::InstancePtr&& response) override { - auto message = ResponsePtr(dynamic_cast( + auto message = ResponsePtr(Envoy::Protobuf::DynamicCastMessage( Internal::parseMessageUntyped(std::make_unique(), std::move(response)) .release())); if (!message) { @@ -139,7 +139,7 @@ template class AsyncClient /* : public Raw void reset() { client_.reset(); } private: - RawAsyncClientSharedPtr client_{}; + RawAsyncClientSharedPtr client_; }; } // namespace Grpc diff --git a/source/common/http/BUILD b/source/common/http/BUILD index 37facab6308b6..6857fa1b1c6bb 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -82,6 +82,9 @@ envoy_cc_library( envoy_cc_library( name = "character_set_validation_lib", hdrs = ["character_set_validation.h"], + deps = [ + "@abseil-cpp//absl/strings:string_view", + ], ) envoy_cc_library( @@ -94,6 +97,7 @@ envoy_cc_library( ":status_lib", ":utility_lib", "//envoy/event:deferred_deletable", + "//envoy/http:client_codec_factory_interface", "//envoy/http:codec_interface", "//envoy/http:header_validator_interface", "//envoy/network:connection_interface", @@ -572,10 +576,12 @@ envoy_cc_library( ":message_lib", "//bazel/external/http_parser", "//envoy/common:regex_interface", + "//envoy/formatter:substitution_formatter_interface", "//envoy/http:codes_interface", "//envoy/http:filter_interface", "//envoy/http:header_map_interface", "//envoy/http:query_params_interface", + "//envoy/stream_info:stream_info_interface", "//source/common/buffer:buffer_lib", "//source/common/common:assert_lib", "//source/common/common:empty_string", @@ -587,7 +593,6 @@ envoy_cc_library( "//source/common/protobuf:utility_lib", "//source/common/runtime:runtime_features_lib", "@abseil-cpp//absl/container:node_hash_set", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@quiche//:http2_adapter_http2_protocol", @@ -630,7 +635,6 @@ envoy_cc_library( "//envoy/http:header_map_interface", "//source/common/common:logger_lib", "//source/common/runtime:runtime_features_lib", - "@abseil-cpp//absl/types:optional", "@googleurl//url", ], ) @@ -693,7 +697,6 @@ envoy_cc_library( "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:string_view", - "@abseil-cpp//absl/types:optional", "@abseil-cpp//absl/types:span", ], ) diff --git a/source/common/http/async_client_impl.cc b/source/common/http/async_client_impl.cc index 52b7e85f25b5e..79e19576467a8 100644 --- a/source/common/http/async_client_impl.cc +++ b/source/common/http/async_client_impl.cc @@ -168,7 +168,7 @@ AsyncStreamImpl::AsyncStreamImpl(AsyncClientImpl& parent, AsyncClient::StreamCal void AsyncStreamImpl::sendLocalReply(Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) { stream_info_.setResponseCodeDetails(details); if (encoded_response_headers_) { diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index f73c462cad375..c60c172f1cf4d 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -164,13 +164,13 @@ class AsyncStreamImpl : public virtual AsyncClient::Stream, AsyncClientImpl& parent_; // Callback to listen for stream destruction. - absl::optional destructor_callback_; + std::optional destructor_callback_; // Callback to listen for low/high/overflow watermark events. - absl::optional> watermark_callbacks_; + std::optional> watermark_callbacks_; bool complete_{}; const bool discard_response_body_; const bool new_async_client_retry_logic_{}; - absl::optional buffer_limit_{absl::nullopt}; + std::optional buffer_limit_{std::nullopt}; private: void cleanup(); @@ -223,7 +223,7 @@ class AsyncStreamImpl : public virtual AsyncClient::Stream, void modifyDecodingBuffer(std::function) override {} void sendLocalReply(Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) override; // The async client won't pause if sending 1xx headers so simply swallow any. void encode1xxHeaders(ResponseHeaderMapPtr&&) override {} diff --git a/source/common/http/character_set_validation.h b/source/common/http/character_set_validation.h index 1f6973eae3a3d..cfa13476fa142 100644 --- a/source/common/http/character_set_validation.h +++ b/source/common/http/character_set_validation.h @@ -3,24 +3,64 @@ #include #include +#include "absl/strings/string_view.h" + // A set of tables for validating that a character is in a specific // character set. Used to validate RFC compliance for various HTTP protocol elements. namespace Envoy { namespace Http { -inline constexpr bool testCharInTable(const std::array& table, char c) { - // CPU cache friendly version of a lookup in a bit table of size 256. - // The table is organized as 8 32 bit words. - // This function looks up a bit from the `table` at the index `c`. - // This function is used to test whether a character `c` is allowed - // or not based on the value of a bit at index `c`. - uint8_t tmp = static_cast(c); - // The `tmp >> 5` determines which of the 8 uint32_t words has the bit at index `uc`. - // The `0x80000000 >> (tmp & 0x1f)` determines the index of the bit within the 32 bit word. - return (table[tmp >> 5] & (0x80000000 >> (tmp & 0x1f))) != 0; -} +struct CharTable { + const std::array table; + + static constexpr uint32_t row(char c) { return static_cast(c) >> 5; } + static constexpr uint32_t mask(char c) { return 0x80000000 >> (static_cast(c) & 0x1f); } + constexpr bool hasChar(char c) const { return (table[row(c)] & mask(c)) != 0; } + static constexpr void set(std::array& table, char c) { table[row(c)] |= mask(c); } + static constexpr CharTable fromChars(absl::string_view chars) { + std::array table{}; + for (char c : chars) { + set(table, c); + } + return {table}; + } + constexpr CharTable operator|(const CharTable& o) const { + std::array result; + for (int i = 0; i < 8; i++) { + result[i] = table[i] | o.table[i]; + } + return {result}; + } + constexpr CharTable operator&(const CharTable& o) const { + std::array result; + for (int i = 0; i < 8; i++) { + result[i] = table[i] & o.table[i]; + } + return {result}; + } + constexpr CharTable operator~() const { + std::array result; + for (int i = 0; i < 8; i++) { + result[i] = ~table[i]; + } + return {result}; + } +}; +namespace CharTables { +// Bits 65 (A) to 90 (Z) +inline constexpr CharTable kUppercase{{0, 0, 0b01111111111111111111111111100000, 0, 0, 0, 0, 0}}; +// Bits 97 (a) to 122 (z) +inline constexpr CharTable kLowercase{{0, 0, 0, 0b01111111111111111111111111100000, 0, 0, 0, 0}}; +// Bits 33 (!) to 127 (~). +inline constexpr CharTable kPrintable{{0, 0x7fffffff, 0xffffffff, 0xfffffffe, 0, 0, 0, 0}}; +// Bits 129 to 255. +inline constexpr CharTable kExtendedAscii{ + {0, 0, 0, 0, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}}; +// Bits 48 ('0') to 57 ('9') +inline constexpr CharTable kDigits{{0, 0b00000000000000001111111111000000, 0, 0, 0, 0, 0, 0}}; +inline constexpr CharTable kAlphanumeric = kUppercase | kLowercase | kDigits; // Header name character table. // From RFC 9110, https://www.rfc-editor.org/rfc/rfc9110.html#section-5.1: // @@ -33,21 +73,8 @@ inline constexpr bool testCharInTable(const std::array& table, char // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" // / DIGIT / ALPHA // SPELLCHECKER(on) -inline constexpr std::array kGenericHeaderNameCharTable = { - // control characters - 0b00000000000000000000000000000000, - // !"#$%&'()*+,-./0123456789:;<=>? - 0b01011111001101101111111111000000, - //@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ - 0b01111111111111111111111111100011, - //`abcdefghijklmnopqrstuvwxyz{|}~ - 0b11111111111111111111111111101010, - // extended ascii - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, -}; +inline constexpr CharTable kGenericHeaderName = + kAlphanumeric | CharTable::fromChars("!#$%&'*+-.^_`|~"); // A URI query and fragment character table. From RFC 3986: // https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 @@ -55,22 +82,19 @@ inline constexpr std::array kGenericHeaderNameCharTable = { // SPELLCHECKER(off) // query = *( pchar / "/" / "?" ) // fragment = *( pchar / "/" / "?" ) +// +// pchar = unreserved / pct-encoded / sub-delims / ":" / "@" +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// pct-encoded = "%" HEXDIG HEXDIG +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" // SPELLCHECKER(on) -inline constexpr std::array kUriQueryAndFragmentCharTable = { - // control characters - 0b00000000000000000000000000000000, - // !"#$%&'()*+,-./0123456789:;<=>? - 0b01001111111111111111111111110101, - //@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ - 0b11111111111111111111111111100001, - //`abcdefghijklmnopqrstuvwxyz{|}~ - 0b01111111111111111111111111100010, - // extended ascii - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, -}; +inline constexpr CharTable kUriQueryAndFragment = + kAlphanumeric | CharTable::fromChars("/?" + ":@" + "-._~" + "%" + "!$&'()*+,;="); +} // namespace CharTables } // namespace Http } // namespace Envoy diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index e8c602d32b141..bd6b260b9467f 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -3,6 +3,7 @@ #include #include +#include "envoy/http/client_codec_factory.h" #include "envoy/http/codec.h" #include "source/common/common/enum_to_int.h" @@ -291,45 +292,59 @@ CodecClientProd::CodecClientProd(CodecType type, Network::ClientConnectionPtr&& const Network::TransportSocketOptionsConstSharedPtr& options, bool should_connect) : CodecClient(type, std::move(connection), host, dispatcher) { - switch (type) { - case CodecType::HTTP1: { - // If the transport socket indicates this is being proxied, inform the HTTP/1.1 codec. It will - // send fully qualified URLs iff the underlying transport is plaintext. - bool proxied = false; - if (options && options->http11ProxyInfo().has_value()) { - proxied = true; - } - codec_ = std::make_unique( - *connection_, host->cluster().http1CodecStats(), *this, - host->cluster().httpProtocolOptions().http1Settings(), - host->cluster().maxResponseHeadersKb(), host->cluster().maxResponseHeadersCount(), proxied); - break; + // A per-cluster upstream codec factory (configured via typed_extension_protocol_options) may + // build a custom client codec for this connection. It returns nullptr to defer to the stock + // codec built below. The stock codec is only constructed when no custom codec is used, because + // constructing it has immediate side effects on the connection (e.g. the HTTP/2 codec writes + // SETTINGS), so building one we would discard could corrupt the connection. + if (auto factory = host->cluster().upstreamHttpClientCodecFactory(); factory.has_value()) { + codec_ = factory->createClientCodec(Http::ClientCodecFactory::Context{ + type, *connection_, *this, host->cluster(), random_generator, options}); } - case CodecType::HTTP2: - codec_ = std::make_unique( - *connection_, *this, host->cluster().http2CodecStats(), random_generator, - host->cluster().httpProtocolOptions().http2Options(), - host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), - host->cluster().maxResponseHeadersCount(), Http2::ProdNghttp2SessionFactory::get()); - break; - case CodecType::HTTP3: { + + if (codec_ == nullptr) { + switch (type) { + case CodecType::HTTP1: { + // If the transport socket indicates this is being proxied, inform the HTTP/1.1 codec. It will + // send fully qualified URLs iff the underlying transport is plaintext. + bool proxied = false; + if (options && options->http11ProxyInfo().has_value()) { + proxied = true; + } + codec_ = std::make_unique( + *connection_, host->cluster().http1CodecStats(), *this, + host->cluster().httpProtocolOptions().http1Settings(), + host->cluster().maxResponseHeadersKb(), host->cluster().maxResponseHeadersCount(), + proxied); + break; + } + case CodecType::HTTP2: + codec_ = std::make_unique( + *connection_, *this, host->cluster().http2CodecStats(), random_generator, + host->cluster().httpProtocolOptions().http2Options(), + host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), + host->cluster().maxResponseHeadersCount(), Http2::ProdNghttp2SessionFactory::get()); + break; + case CodecType::HTTP3: { #ifdef ENVOY_ENABLE_QUIC - auto& quic_session = dynamic_cast(*connection_); - codec_ = std::make_unique( - quic_session, *this, host->cluster().http3CodecStats(), - host->cluster().httpProtocolOptions().http3Options(), - host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), - host->cluster().maxResponseHeadersCount()); - // Initialize the session after max request header size is changed in above http client - // connection creation. - quic_session.Initialize(); - break; + auto& quic_session = dynamic_cast(*connection_); + codec_ = std::make_unique( + quic_session, *this, host->cluster().http3CodecStats(), + host->cluster().httpProtocolOptions().http3Options(), + host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), + host->cluster().maxResponseHeadersCount()); + // Initialize the session after max request header size is changed in above http client + // connection creation. + quic_session.Initialize(); + break; #else - // Should be blocked by configuration checking at an earlier point. - PANIC("unexpected"); + // Should be blocked by configuration checking at an earlier point. + PANIC("unexpected"); #endif + } + } } - } + if (should_connect) { connect(); } diff --git a/source/common/http/codec_client.h b/source/common/http/codec_client.h index 0eec3f2618733..7ca9e4bba9692 100644 --- a/source/common/http/codec_client.h +++ b/source/common/http/codec_client.h @@ -155,9 +155,6 @@ class CodecClient : protected Logger::Loggable, bool connectCalled() const { return connect_called_; } - // Get the underlying connection of the codec client for lifetimeCallbacks. - const Network::Connection& connection() const { return *connection_; } - protected: /** * Create a codec client and connect to a remote host/port. @@ -216,7 +213,7 @@ class CodecClient : protected Logger::Loggable, Network::ClientConnectionPtr connection_; ClientConnectionPtr codec_; Event::TimerPtr idle_timer_; - const absl::optional idle_timeout_; + const std::optional idle_timeout_; const bool enable_idle_timer_only_when_connected_; private: diff --git a/source/common/http/codec_wrappers.h b/source/common/http/codec_wrappers.h index 418c593ac5933..c27fe4af01ce8 100644 --- a/source/common/http/codec_wrappers.h +++ b/source/common/http/codec_wrappers.h @@ -76,6 +76,13 @@ class ResponseDecoderWrapper : public ResponseDecoderImplBase { } } + OptRef downstreamWebTransportSession() override { + if (Http::ResponseDecoder* inner = getInnerDecoder()) { + return inner->downstreamWebTransportSession(); + } + return {}; + } + protected: ResponseDecoderWrapper(ResponseDecoder& inner) : inner_(&inner) {} diff --git a/source/common/http/conn_manager_config.h b/source/common/http/conn_manager_config.h index c5bcbda14a8c0..9d29a13afdadc 100644 --- a/source/common/http/conn_manager_config.h +++ b/source/common/http/conn_manager_config.h @@ -220,9 +220,9 @@ class ConnectionManagerConfig { virtual const AccessLog::InstanceSharedPtrVector& accessLogs() PURE; /** - * @return const absl::optional& the interval to flush the access logs. + * @return const std::optional& the interval to flush the access logs. */ - virtual const absl::optional& accessLogFlushInterval() PURE; + virtual const std::optional& accessLogFlushInterval() PURE; // If set to true, access log will be flushed when a new HTTP request is received, after request // headers have been evaluated, and before attempting to establish a connection with the upstream. @@ -253,7 +253,10 @@ class ConnectionManagerConfig { /** * @return the time in milliseconds the connection manager will wait between issuing a "shutdown - * notice" to the time it will issue a full GOAWAY and not accept any new streams. + * notice" to the time it will issue a full GOAWAY and not accept any new streams. If + * ``drain_timeout_jitter`` is configured, each call returns the base timeout extended + * by a random amount up to ``drainTimeout * jitter / 100``. The jittering is an + * implementation detail and not exposed as a separate interface method. */ virtual std::chrono::milliseconds drainTimeout() const PURE; @@ -282,7 +285,7 @@ class ConnectionManagerConfig { /** * @return optional idle timeout for incoming connection manager connections. */ - virtual absl::optional idleTimeout() const PURE; + virtual std::optional idleTimeout() const PURE; /** * @return if the connection manager does routing base on router config, e.g. a Server::Admin impl @@ -291,9 +294,14 @@ class ConnectionManagerConfig { virtual bool isRoutable() const PURE; /** - * @return optional maximum connection duration timeout for manager connections. + * @return optional maximum connection duration timeout for manager connections. If + * ``max_connection_duration_jitter`` is configured, each call returns the + * base duration extended by a random amount up to + * ``max_connection_duration * jitter / 100``. The jittering is an + * implementation detail and not exposed as a separate interface method; + * callers should arm their timer with whatever value this returns. */ - virtual absl::optional maxConnectionDuration() const PURE; + virtual std::optional maxConnectionDuration() const PURE; /** * @return whether maxConnectionDuration allows HTTP1 clients to choose when to close connection @@ -321,7 +329,7 @@ class ConnectionManagerConfig { * @return per-stream flush timeout for incoming connection manager connections. Zero indicates a * disabled idle timeout. */ - virtual absl::optional streamFlushTimeout() const PURE; + virtual std::optional streamFlushTimeout() const PURE; /** * @return request timeout for incoming connection manager connections. Zero indicates @@ -344,7 +352,7 @@ class ConnectionManagerConfig { /** * @return maximum duration time to keep alive stream */ - virtual absl::optional maxStreamDuration() const PURE; + virtual std::optional maxStreamDuration() const PURE; /** * @return Router::RouteConfigProvider* the configuration provider used to acquire a route @@ -378,9 +386,9 @@ class ConnectionManagerConfig { serverHeaderTransformation() const PURE; /** - * @return const absl::optional the scheme name to write into requests. + * @return const std::optional the scheme name to write into requests. */ - virtual const absl::optional& schemeToSet() const PURE; + virtual const std::optional& schemeToSet() const PURE; /** * @return bool whether the scheme should be overwritten to match the upstream transport protocol. @@ -422,7 +430,7 @@ class ConnectionManagerConfig { virtual bool skipXffAppend() const PURE; /** - * @return const absl::optional& value of via header to add to requests and response + * @return const std::optional& value of via header to add to requests and response * headers if set. */ virtual const std::string& via() const PURE; @@ -461,7 +469,7 @@ class ConnectionManagerConfig { * be enabled. User agent will only overwritten if it doesn't already exist. If enabled, * the same user agent will be written to the x-envoy-downstream-service-cluster header. */ - virtual const absl::optional& userAgent() PURE; + virtual const std::optional& userAgent() PURE; /** * @return TracerSharedPtr Tracer to use. diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index 77e44fe6fae18..27db3598417de 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -133,6 +133,10 @@ ConnectionManagerImpl::ConnectionManagerImpl( overload_manager.getLoadShedPoint(Server::LoadShedPointName::get().HcmDecodeHeaders)), hcm_ondata_creating_codec_( overload_manager.getLoadShedPoint(Server::LoadShedPointName::get().HcmCodecCreation)), + should_send_go_away_on_dispatch_(overload_manager.getLoadShedPoint( + Server::LoadShedPointName::get().H2ServerGoAwayOnDispatch)), + should_send_go_away_and_close_on_dispatch_(overload_manager.getLoadShedPoint( + Server::LoadShedPointName::get().H2ServerGoAwayAndCloseOnDispatch)), overload_stop_accepting_requests_ref_( overload_state_.getState(Server::OverloadActionNames::get().StopAcceptingRequests)), overload_disable_keepalive_ref_( @@ -155,6 +159,13 @@ ConnectionManagerImpl::ConnectionManagerImpl( ENVOY_LOG_ONCE_IF(trace, hcm_ondata_creating_codec_ == nullptr, "LoadShedPoint envoy.load_shed_points.hcm_ondata_creating_codec is not found. " "Is it configured?"); + ENVOY_LOG_ONCE_IF(trace, should_send_go_away_on_dispatch_ == nullptr, + "LoadShedPoint envoy.load_shed_points.http2_server_go_away_on_dispatch is not " + "found. Is it configured?"); + ENVOY_LOG_ONCE_IF( + trace, should_send_go_away_and_close_on_dispatch_ == nullptr, + "LoadShedPoint envoy.load_shed_points.http2_server_go_away_and_close_on_dispatch is not " + "found. Is it configured?"); } const ResponseHeaderMap& ConnectionManagerImpl::continueHeader() { @@ -190,7 +201,7 @@ void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCal std::make_unique(Network::ProxyProtocolData{ read_callbacks_->connection().connectionInfoProvider().remoteAddress(), read_callbacks_->connection().connectionInfoProvider().localAddress()}), - StreamInfo::FilterState::StateType::Mutable, StreamInfo::FilterState::LifeSpan::Connection); + StreamInfo::FilterState::LifeSpan::Connection); } if (config_->idleTimeout()) { @@ -200,11 +211,11 @@ void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCal connection_idle_timer_->enableTimer(config_->idleTimeout().value()); } - if (config_->maxConnectionDuration()) { + if (auto max_connection_duration = config_->maxConnectionDuration(); max_connection_duration) { connection_duration_timer_ = dispatcher_->createScaledTimer(Event::ScaledTimerType::HttpDownstreamMaxConnectionTimeout, [this]() -> void { onConnectionDurationTimeout(); }); - connection_duration_timer_->enableTimer(config_->maxConnectionDuration().value()); + connection_duration_timer_->enableTimer(max_connection_duration.value()); } read_callbacks_->connection().setDelayedCloseTimeout(config_->delayedCloseTimeout()); @@ -250,7 +261,7 @@ void ConnectionManagerImpl::checkForDeferredClose(bool skip_delay_close) { if (drain_state_ == DrainState::Closing && streams_.empty() && !codec_->wantsToWrite()) { // We are closing a draining connection with no active streams and the codec has // nothing to write. - doConnectionClose(close, absl::nullopt, + doConnectionClose(close, std::nullopt, StreamInfo::LocalCloseReasons::get().DeferredCloseOnDrainedConnection); } } @@ -514,6 +525,20 @@ Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance& data, bool createCodec(data); } + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_fix_goaway_loadshed_point")) { + if (should_send_go_away_and_close_on_dispatch_ != nullptr && + should_send_go_away_and_close_on_dispatch_->shouldShedLoad()) { + sendGoAwayAndClose(/*graceful=*/false); + handleCodecOverloadError( + "Load shed point http2_server_go_away_and_close_on_dispatch triggered"); + return Network::FilterStatus::StopIteration; + } + if (should_send_go_away_on_dispatch_ != nullptr && + should_send_go_away_on_dispatch_->shouldShedLoad()) { + sendGoAwayAndClose(/*graceful=*/true); + } + } + bool redispatch; do { redispatch = false; @@ -571,7 +596,7 @@ Network::FilterStatus ConnectionManagerImpl::onNewConnection() { } void ConnectionManagerImpl::resetAllStreams( - absl::optional response_flag, absl::string_view details) { + std::optional response_flag, absl::string_view details) { while (!streams_.empty()) { // Mimic a downstream reset in this case. We must also remove callbacks here. Though we are // about to close the connection and will disable further reads, it is possible that flushing @@ -626,14 +651,14 @@ void ConnectionManagerImpl::onEvent(Network::ConnectionEvent event) { // NOTE: In the case where a local close comes from outside the filter, this will cause any // stream closures to increment remote close stats. We should do better here in the future, // via the pre-close callback mentioned above. - doConnectionClose(absl::nullopt, StreamInfo::CoreResponseFlag::DownstreamConnectionTermination, + doConnectionClose(std::nullopt, StreamInfo::CoreResponseFlag::DownstreamConnectionTermination, details); } } void ConnectionManagerImpl::doConnectionClose( - absl::optional close_type, - absl::optional response_flag, absl::string_view details) { + std::optional close_type, + std::optional response_flag, absl::string_view details) { if (connection_idle_timer_) { connection_idle_timer_->disableTimer(); connection_idle_timer_.reset(); @@ -662,6 +687,11 @@ void ConnectionManagerImpl::doConnectionClose( stats_.named_.downstream_cx_destroy_active_rq_.inc(); user_agent_.onConnectionDestroy(event, true); + // Record the downstream connection end time point for COMMON_DURATION access logging. + for (auto& stream : streams_) { + stream->filter_manager_.streamInfo().downstreamTiming().onDownstreamConnectionEnd( + time_source_); + } // Note that resetAllStreams() does not actually write anything to the wire. It just resets // all upstream streams and their filter stacks. Thus, there are no issues around recursive // entry. @@ -677,7 +707,7 @@ bool ConnectionManagerImpl::isPrematureRstStream(const ActiveStream& stream) con // Check if the request was prematurely reset, by comparing its lifetime to the configured // threshold. ASSERT(!stream.state_.is_internally_destroyed_); - absl::optional duration = + std::optional duration = stream.filter_manager_.streamInfo().currentDuration(); // Check if request lifetime is longer than the premature reset threshold. @@ -742,7 +772,7 @@ void ConnectionManagerImpl::maybeDrainDueToPrematureResets() { // Mark the the connection has been drained due to too many premature resets. drained_due_to_premature_resets_ = true; - doConnectionClose(Network::ConnectionCloseType::Abort, absl::nullopt, + doConnectionClose(Network::ConnectionCloseType::Abort, std::nullopt, "too_many_premature_resets"); } } @@ -758,7 +788,7 @@ void ConnectionManagerImpl::onIdleTimeout() { if (!codec_) { // No need to delay close after flushing since an idle timeout has already fired. Attempt to // write out buffered data one last time and issue a local close if successful. - doConnectionClose(Network::ConnectionCloseType::FlushWrite, absl::nullopt, + doConnectionClose(Network::ConnectionCloseType::FlushWrite, std::nullopt, StreamInfo::LocalCloseReasons::get().IdleTimeoutOnConnection); } else if (drain_state_ == DrainState::NotDraining) { startDrainSequence(); @@ -794,7 +824,7 @@ void ConnectionManagerImpl::onDrainTimeout() { } void ConnectionManagerImpl::sendGoAwayAndClose(bool graceful) { - ENVOY_CONN_LOG(trace, "connection manager sendGoAwayAndClose was triggerred from filters.", + ENVOY_CONN_LOG(trace, "connection manager sendGoAwayAndClose was triggered.", read_callbacks_->connection()); if (go_away_sent_) { return; @@ -817,7 +847,7 @@ void ConnectionManagerImpl::sendGoAwayAndClose(bool graceful) { codec_->shutdownNotice(); codec_->goAway(); go_away_sent_ = true; - doConnectionClose(Network::ConnectionCloseType::FlushWriteAndDelay, absl::nullopt, + doConnectionClose(Network::ConnectionCloseType::FlushWriteAndDelay, std::nullopt, "forced_goaway"); } } @@ -840,7 +870,7 @@ void ConnectionManagerImpl::chargeTracingStats(const Tracing::Reason& tracing_re } } -absl::optional +std::optional ConnectionManagerImpl::HttpStreamIdProviderImpl::toStringView() const { if (parent_.request_headers_ == nullptr) { return {}; @@ -849,7 +879,7 @@ ConnectionManagerImpl::HttpStreamIdProviderImpl::toStringView() const { return parent_.connection_manager_.config_->requestIDExtension()->get(*parent_.request_headers_); } -absl::optional ConnectionManagerImpl::HttpStreamIdProviderImpl::toInteger() const { +std::optional ConnectionManagerImpl::HttpStreamIdProviderImpl::toInteger() const { if (parent_.request_headers_ == nullptr) { return {}; } @@ -867,7 +897,7 @@ ConnectionManagerImpl::ActiveStream::ActiveStream(ConnectionManagerImpl& connect Buffer::BufferMemoryAccountSharedPtr account) : connection_manager_(connection_manager), connection_manager_tracing_config_(connection_manager_.config_->tracingConfig() == nullptr - ? absl::nullopt + ? std::nullopt : makeOptRef( *connection_manager_.config_->tracingConfig())), stream_id_(connection_manager.random_generator_.random()), @@ -904,6 +934,10 @@ ConnectionManagerImpl::ActiveStream::ActiveStream(ConnectionManagerImpl& connect filter_manager_.streamInfo().setShouldSchemeMatchUpstream( connection_manager.config_->shouldSchemeMatchUpstream()); + // Record the downstream connection begin time point for COMMON_DURATION access logging. + filter_manager_.streamInfo().downstreamTiming().setDownstreamConnectionBegin( + connection_manager_.read_callbacks_->connection().streamInfo().startTimeMonotonic()); + // TODO(chaoqin-li1123): can this be moved to the on demand filter? auto factory = Envoy::Config::Utility::getFactoryByName( kRouteFactoryName); @@ -1030,7 +1064,7 @@ void ConnectionManagerImpl::ActiveStream::onIdleTimeout() { filter_manager_.streamInfo().setResponseFlag(StreamInfo::CoreResponseFlag::StreamIdleTimeout); sendLocalReply( Http::Utility::maybeRequestTimeoutCode(filter_manager_.hasLastDownstreamByteReceived()), - "stream timeout", nullptr, absl::nullopt, + "stream timeout", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().StreamIdleTimeout); } @@ -1038,7 +1072,7 @@ void ConnectionManagerImpl::ActiveStream::onRequestTimeout() { connection_manager_.stats_.named_.downstream_rq_timeout_.inc(); sendLocalReply( Http::Utility::maybeRequestTimeoutCode(filter_manager_.hasLastDownstreamByteReceived()), - "request timeout", nullptr, absl::nullopt, + "request timeout", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().RequestOverallTimeout); } @@ -1046,7 +1080,7 @@ void ConnectionManagerImpl::ActiveStream::onRequestHeaderTimeout() { connection_manager_.stats_.named_.downstream_rq_header_timeout_.inc(); sendLocalReply( Http::Utility::maybeRequestTimeoutCode(filter_manager_.hasLastDownstreamByteReceived()), - "request header timeout", nullptr, absl::nullopt, + "request header timeout", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().RequestHeaderTimeout); } @@ -1075,7 +1109,7 @@ void ConnectionManagerImpl::ActiveStream::chargeStats(const ResponseHeaderMap& h } // No response is sent back downstream for internal redirects, so don't charge downstream stats. - const absl::optional& response_code_details = + const std::optional& response_code_details = filter_manager_.streamInfo().responseCodeDetails(); if (response_code_details.has_value() && response_code_details == Envoy::StreamInfo::ResponseCodeDetails::get().InternalRedirect) { @@ -1217,7 +1251,7 @@ bool ConnectionManagerImpl::ActiveStream::validateHeaders() { Code response_code = failure_details == Http1ResponseCodeDetail::get().InvalidTransferEncoding ? Code::NotImplemented : Code::BadRequest; - absl::optional grpc_status; + std::optional grpc_status; if (redirect && !is_grpc) { response_code = Code::TemporaryRedirect; modify_headers = [new_path = request_headers_->Path()->value().getStringView()]( @@ -1268,7 +1302,7 @@ bool ConnectionManagerImpl::ActiveStream::validateTrailers(RequestTrailerMap& tr } Code response_code = Code::BadRequest; - absl::optional grpc_status; + std::optional grpc_status; if (Grpc::Common::hasGrpcContentType(*request_headers_)) { grpc_status = Grpc::Status::WellKnownGrpcStatus::Internal; } @@ -1390,7 +1424,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt Http::Headers::get().EnvoyOverloadedValues.True); } }, - absl::nullopt, StreamInfo::ResponseCodeDetails::get().Overload); + std::nullopt, StreamInfo::ResponseCodeDetails::get().Overload); return; } @@ -1413,16 +1447,16 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt if (!request_headers_->Host()) { // Require host header. For HTTP/1.1 Host has already been translated to :authority. - sendLocalReply(Code::BadRequest, "", nullptr, absl::nullopt, + sendLocalReply(Code::BadRequest, "", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().MissingHost); return; } // Apply header sanity checks. - absl::optional> error = + std::optional> error = HeaderUtility::requestHeadersValid(*request_headers_); - if (error != absl::nullopt) { - sendLocalReply(Code::BadRequest, "", nullptr, absl::nullopt, error.value().get()); + if (error != std::nullopt) { + sendLocalReply(Code::BadRequest, "", nullptr, std::nullopt, error.value().get()); if (!response_encoder_->streamErrorOnInvalidHttpMessage()) { connection_manager_.handleCodecError(error.value().get()); } @@ -1435,7 +1469,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt // is enabled on the HCM. if ((!HeaderUtility::isConnect(*request_headers_) || request_headers_->Path()) && request_headers_->getPathValue().empty()) { - sendLocalReply(Code::NotFound, "", nullptr, absl::nullopt, + sendLocalReply(Code::NotFound, "", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().MissingPath); return; } @@ -1443,7 +1477,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt // Rewrite the host of CONNECT-UDP requests. if (HeaderUtility::isConnectUdpRequest(*request_headers_) && !HeaderUtility::rewriteAuthorityForConnectUdp(*request_headers_)) { - sendLocalReply(Code::NotFound, "The path is incorrect for CONNECT-UDP", nullptr, absl::nullopt, + sendLocalReply(Code::NotFound, "The path is incorrect for CONNECT-UDP", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().InvalidPath); return; } @@ -1451,7 +1485,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt // Currently we only support relative paths at the application layer. if (!request_headers_->getPathValue().empty() && request_headers_->getPathValue()[0] != '/') { connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc(); - sendLocalReply(Code::NotFound, "", nullptr, absl::nullopt, + sendLocalReply(Code::NotFound, "", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().AbsolutePath); return; } @@ -1467,7 +1501,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt (action == ConnectionManagerUtility::NormalizePathAction::Redirect && Grpc::Common::hasGrpcContentType(*request_headers_))) { connection_manager_.stats_.named_.downstream_rq_failed_path_normalization_.inc(); - sendLocalReply(Code::BadRequest, "", nullptr, absl::nullopt, + sendLocalReply(Code::BadRequest, "", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().PathNormalizationFailed); return; } else if (action == ConnectionManagerUtility::NormalizePathAction::Redirect) { @@ -1478,7 +1512,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt Http::ResponseHeaderMap& response_headers) -> void { response_headers.addReferenceKey(Http::Headers::get().Location, new_path); }, - absl::nullopt, StreamInfo::ResponseCodeDetails::get().PathNormalizationFailed); + std::nullopt, StreamInfo::ResponseCodeDetails::get().PathNormalizationFailed); return; } @@ -1491,7 +1525,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt filter_manager_.streamInfo().filterState()->setData( Router::OriginalConnectPort::key(), std::make_unique(optional_port.value()), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::Request); + StreamInfo::FilterState::LifeSpan::Request); } if (!state_.is_internally_created_) { // Only sanitize headers on first pass. @@ -1506,7 +1540,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt const auto& reject_request_params = mutate_result.reject_request.value(); connection_manager_.stats_.named_.downstream_rq_rejected_via_ip_detection_.inc(); sendLocalReply(reject_request_params.response_code, reject_request_params.body, nullptr, - absl::nullopt, + std::nullopt, StreamInfo::ResponseCodeDetails::get().OriginalIPDetectionFailed); return; } @@ -1549,7 +1583,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapSharedPt // contains a smuggled HTTP request. filter_manager_.streamInfo().setShouldDrainConnectionUponCompletion(true); connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc(); - sendLocalReply(Code::Forbidden, "", nullptr, absl::nullopt, + sendLocalReply(Code::Forbidden, "", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().UpgradeFailed); return; } @@ -1847,7 +1881,7 @@ void ConnectionManagerImpl::ActiveStream::requestRouteConfigUpdate( } } -absl::optional ConnectionManagerImpl::ActiveStream::routeConfig() { +std::optional ConnectionManagerImpl::ActiveStream::routeConfig() { if (connection_manager_.config_->routeConfigProvider() != nullptr) { return {connection_manager_.config_->routeConfigProvider()->configCast()}; } @@ -2433,7 +2467,7 @@ void ConnectionManagerImpl::ActiveStream::clearRouteCache() { } setCachedRoute({}); - cached_cluster_info_ = absl::optional(); + cached_cluster_info_ = std::optional(); } void ConnectionManagerImpl::ActiveStream::refreshRouteCluster() { @@ -2457,8 +2491,23 @@ void ConnectionManagerImpl::ActiveStream::refreshRouteCluster() { } } +void ConnectionManagerImpl::ActiveStream::recreateClusterInfo() { + if (!hasCachedRoute()) { + return; + } + if (const auto* entry = (*cached_route_)->routeEntry(); entry != nullptr) { + if (!cached_cluster_info_.has_value() || cached_cluster_info_.value() == nullptr || + (*cached_cluster_info_)->name() != entry->clusterName()) { + auto* cluster = + connection_manager_.cluster_manager_.getThreadLocalCluster(entry->clusterName()); + cached_cluster_info_ = (nullptr == cluster) ? nullptr : cluster->info(); + filter_manager_.streamInfo().setUpstreamClusterInfo(cached_cluster_info_.value()); + } + } +} + void ConnectionManagerImpl::ActiveStream::setCachedRoute( - absl::optional&& route) { + std::optional&& route) { if (hasCachedRoute()) { // The configuration of the route may be referenced by some filters. // Cache the route to avoid it being destroyed before the stream is destroyed. @@ -2482,7 +2531,6 @@ void ConnectionManagerImpl::ActiveStream::recreateStream( StreamInfo::FilterStateSharedPtr filter_state) { ENVOY_EXECUTION_SCOPE(trackedStream(), active_span_.get()); ResponseEncoder* response_encoder = response_encoder_; - response_encoder_ = nullptr; Buffer::InstancePtr request_data = std::make_unique(); const auto& buffered_request_data = filter_manager_.bufferedRequestData(); @@ -2491,6 +2539,10 @@ void ConnectionManagerImpl::ActiveStream::recreateStream( request_data->move(*buffered_request_data); } + // Null after move(): draining the WatermarkBuffer may synchronously fire + // onDecoderFilterBelowWriteBufferLowWatermark which needs a valid encoder. + response_encoder_ = nullptr; + response_encoder->getStream().removeCallbacks(*this); // This functionally deletes the stream (via deferred delete) so do not diff --git a/source/common/http/conn_manager_impl.h b/source/common/http/conn_manager_impl.h index 0077c748672ad..564f2f544b20c 100644 --- a/source/common/http/conn_manager_impl.h +++ b/source/common/http/conn_manager_impl.h @@ -190,7 +190,7 @@ class ConnectionManagerImpl : Logger::Loggable, StreamInfo::StreamInfo& streamInfo() override { return filter_manager_.streamInfo(); } void sendLocalReply(Code code, absl::string_view body, const std::function& modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) override { return filter_manager_.sendLocalReply(code, body, modify_headers, grpc_status, details); } @@ -242,6 +242,12 @@ class ConnectionManagerImpl : Logger::Loggable, } // FilterManagerCallbacks + OptRef webTransportSession() override { + if (response_encoder_ == nullptr) { + return {}; + } + return response_encoder_->getStream().webTransportSession(); + } void encodeHeaders(ResponseHeaderMap& response_headers, bool end_stream) override; void encode1xxHeaders(ResponseHeaderMap& response_headers) override; void encodeData(Buffer::Instance& data, bool end_stream) override; @@ -316,13 +322,14 @@ class ConnectionManagerImpl : Logger::Loggable, Router::RouteConstSharedPtr routeSharedPtr(const Router::RouteCallback& cb) override; void clearRouteCache() override; void refreshRouteCluster() override; + void recreateClusterInfo() override; void requestRouteConfigUpdate( Http::RouteConfigUpdatedCallbackSharedPtr route_config_updated_cb) override; void setVirtualHostRoute(Router::VirtualHostRoute route); // Set cached route. This method should never be called directly. This is only called in the // setRoute(), clearRouteCache(), and refreshCachedRoute() methods. - void setCachedRoute(absl::optional&& route); + void setCachedRoute(std::optional&& route); // Block the route cache and clear the snapped route config. By doing this the route cache will // not be updated. And if the route config is updated by the RDS, the snapped route config may // be freed before the stream is destroyed. @@ -335,7 +342,7 @@ class ConnectionManagerImpl : Logger::Loggable, return route_cache_blocked_; } - absl::optional routeConfig(); + std::optional routeConfig(); void traceRequest(); // Updates the snapped_route_config_ (by reselecting scoped route configuration), if a scope is @@ -509,7 +516,7 @@ class ConnectionManagerImpl : Logger::Loggable, Router::ScopedConfigConstSharedPtr snapped_scoped_routes_config_; // This is used to track the route that has been cached in the request. And we will keep this // route alive until the request is finished. - absl::optional cached_route_; + std::optional cached_route_; // This is used to track whether the route has been blocked. If the route is blocked, we can not // clear it or refresh it. bool route_cache_blocked_{false}; @@ -528,8 +535,8 @@ class ConnectionManagerImpl : Logger::Loggable, // the lifetime of the route config by itself easily, we could remove this hack. absl::InlinedVector cleared_cached_routes_; - absl::optional cached_cluster_info_; - absl::optional> route_config_update_requester_; + std::optional cached_cluster_info_; + std::optional> route_config_update_requester_; Http::ServerHeaderValidatorPtr header_validator_; friend FilterManager; @@ -580,8 +587,8 @@ class ConnectionManagerImpl : Logger::Loggable, HttpStreamIdProviderImpl(ActiveStream& parent) : parent_(parent) {} // StreamInfo::StreamIdProvider - absl::optional toStringView() const override; - absl::optional toInteger() const override; + std::optional toStringView() const override; + std::optional toInteger() const override; ActiveStream& parent_; }; @@ -605,7 +612,7 @@ class ConnectionManagerImpl : Logger::Loggable, */ void doEndStream(ActiveStream& stream, bool check_for_deferred_close = true); - void resetAllStreams(absl::optional response_flag, + void resetAllStreams(std::optional response_flag, absl::string_view details); void onIdleTimeout(); void onConnectionDurationTimeout(); @@ -616,8 +623,8 @@ class ConnectionManagerImpl : Logger::Loggable, StreamInfo::CoreResponseFlag response_flag); void handleCodecError(absl::string_view error); void handleCodecOverloadError(absl::string_view error); - void doConnectionClose(absl::optional close_type, - absl::optional response_flag, + void doConnectionClose(std::optional close_type, + std::optional response_flag, absl::string_view details); void sendGoAwayAndClose(bool graceful = false); @@ -665,6 +672,8 @@ class ConnectionManagerImpl : Logger::Loggable, Server::ThreadLocalOverloadState& overload_state_; Server::LoadShedPoint* accept_new_http_stream_{nullptr}; Server::LoadShedPoint* hcm_ondata_creating_codec_{nullptr}; + Server::LoadShedPoint* should_send_go_away_on_dispatch_{nullptr}; + Server::LoadShedPoint* should_send_go_away_and_close_on_dispatch_{nullptr}; // References into the overload manager thread local state map. Using these lets us avoid a // map lookup in the hot path of processing each request. const Server::OverloadActionState& overload_stop_accepting_requests_ref_; diff --git a/source/common/http/conn_manager_utility.cc b/source/common/http/conn_manager_utility.cc index fc9f40b840a75..64cbe961d9a51 100644 --- a/source/common/http/conn_manager_utility.cc +++ b/source/common/http/conn_manager_utility.cc @@ -94,12 +94,12 @@ ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec( uint32_t max_request_headers_kb, uint32_t max_request_headers_count, envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action, - Server::OverloadManager& overload_manager) { + Server::OverloadManager& overload_manager, Runtime::Loader& runtime) { if (determineNextProtocol(connection, data) == Utility::AlpnNames::get().Http2) { Http2::CodecStats& stats = Http2::CodecStats::atomicGet(http2_codec_stats, scope); return std::make_unique( connection, callbacks, stats, random, http2_options, max_request_headers_kb, - max_request_headers_count, headers_with_underscores_action, overload_manager); + max_request_headers_count, headers_with_underscores_action, overload_manager, runtime); } else { Http1::CodecStats& stats = Http1::CodecStats::atomicGet(http1_codec_stats, scope); return std::make_unique( @@ -323,7 +323,7 @@ ConnectionManagerUtility::MutateRequestHeadersResult ConnectionManagerUtility::m } mutateXfccRequestHeader(request_headers, stream_info, connection, config); - return {final_remote_address, absl::nullopt}; + return {final_remote_address, std::nullopt}; } void ConnectionManagerUtility::sanitizeTEHeader(RequestHeaderMap& request_headers) { @@ -717,7 +717,14 @@ void ConnectionManagerUtility::mutateResponseHeaders(ResponseHeaderMap& response } if (clear_hop_by_hop) { response_headers.removeTransferEncoding(); - response_headers.removeKeepAlive(); + // Only preserve Keep-Alive for HTTP/1.x downstream connections when the feature is enabled. + // Keep-Alive is an HTTP/1.1 hop-by-hop header with no meaning in HTTP/2 or HTTP/3. + const auto protocol = stream_info.protocol(); + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.preserve_downstream_keepalive") || + !protocol.has_value() || *protocol >= Protocol::Http2) { + response_headers.removeKeepAlive(); + } response_headers.removeProxyConnection(); } @@ -740,7 +747,7 @@ void ConnectionManagerUtility::setProxyStatusHeader(ResponseHeaderMap& response_ // Writing the Proxy-Status header is gated on the existence of // |proxy_status_config|. The |details| field and other internals are generated in // fromStreamInfo(). - if (absl::optional proxy_status = + if (std::optional proxy_status = StreamInfo::ProxyStatusUtils::fromStreamInfo(stream_info); proxy_status.has_value()) { response_headers.appendProxyStatus( @@ -749,7 +756,7 @@ void ConnectionManagerUtility::setProxyStatusHeader(ResponseHeaderMap& response_ ", "); // Apply the recommended response code, if configured and applicable. if (proxy_status_config->set_recommended_response_code()) { - if (absl::optional response_code = + if (std::optional response_code = StreamInfo::ProxyStatusUtils::recommendedHttpStatusCode(*proxy_status); response_code.has_value()) { response_headers.setStatus(std::to_string(enumToInt(*response_code))); @@ -811,18 +818,18 @@ ConnectionManagerUtility::maybeNormalizePath(RequestHeaderMap& request_headers, return final_action; } -absl::optional +std::optional ConnectionManagerUtility::maybeNormalizeHost(RequestHeaderMap& request_headers, const ConnectionManagerConfig& config, uint32_t port) { if (config.shouldStripTrailingHostDot()) { HeaderUtility::stripTrailingHostDot(request_headers); } if (config.stripPortType() == Http::StripPortType::Any) { - return HeaderUtility::stripPortFromHost(request_headers, absl::nullopt); + return HeaderUtility::stripPortFromHost(request_headers, std::nullopt); } else if (config.stripPortType() == Http::StripPortType::MatchingHost) { return HeaderUtility::stripPortFromHost(request_headers, port); } - return absl::nullopt; + return std::nullopt; } } // namespace Http diff --git a/source/common/http/conn_manager_utility.h b/source/common/http/conn_manager_utility.h index c3d20063b6f4d..6503a4ab37f2b 100644 --- a/source/common/http/conn_manager_utility.h +++ b/source/common/http/conn_manager_utility.h @@ -5,6 +5,7 @@ #include "envoy/http/header_map.h" #include "envoy/network/connection.h" +#include "envoy/runtime/runtime.h" #include "envoy/tracing/trace_reason.h" #include "source/common/http/conn_manager_impl.h" @@ -46,7 +47,7 @@ class ConnectionManagerUtility { uint32_t max_request_headers_kb, uint32_t max_request_headers_count, envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action, - Server::OverloadManager& overload_manager); + Server::OverloadManager& overload_manager, Runtime::Loader& runtime); /* The result after calling mutateRequestHeaders(), containing the final remote address. Note that * an extension used for detecting the original IP of the request might decide it should be @@ -54,7 +55,7 @@ class ConnectionManagerUtility { */ struct MutateRequestHeadersResult { Network::Address::InstanceConstSharedPtr final_remote_address; - absl::optional reject_request; + std::optional reject_request; }; /** @@ -125,9 +126,9 @@ class ConnectionManagerUtility { static NormalizePathAction maybeNormalizePath(RequestHeaderMap& request_headers, const ConnectionManagerConfig& config); - static absl::optional maybeNormalizeHost(RequestHeaderMap& request_headers, - const ConnectionManagerConfig& config, - uint32_t port); + static std::optional maybeNormalizeHost(RequestHeaderMap& request_headers, + const ConnectionManagerConfig& config, + uint32_t port); /** * Mutate request headers if request needs to be traced. diff --git a/source/common/http/conn_pool_base.cc b/source/common/http/conn_pool_base.cc index 1e89fcb7f9f90..df542b6417238 100644 --- a/source/common/http/conn_pool_base.cc +++ b/source/common/http/conn_pool_base.cc @@ -104,24 +104,6 @@ void HttpConnPoolImplBase::onPoolReady(Envoy::ConnectionPool::ActiveClient& clie http_client->codec_client_->protocol()); } -void HttpConnPoolImplBase::setLifetimeCallbacks( - OptRef callbacks, std::vector hash_key) { - callbacks_ = callbacks; - hash_key_ = std::move(hash_key); -} - -void HttpConnPoolImplBase::onConnectionOpen(const Network::Connection& connection) { - if (callbacks_.has_value()) { - callbacks_->onConnectionOpen(*this, hash_key_, connection); - } -} - -void HttpConnPoolImplBase::onConnectionDraining(const Network::Connection& connection) { - if (callbacks_.has_value()) { - callbacks_->onConnectionDraining(*this, hash_key_, connection); - } -} - // All streams are 2^31. Client streams are half that, minus stream 0. Just to be on the safe // side we do 2^29. constexpr uint32_t DEFAULT_MAX_STREAMS = 1U << 29; @@ -133,7 +115,6 @@ void MultiplexedActiveClientBase::onGoAway(Http::GoAwayErrorCode) { if (codec_client_->numActiveRequests() == 0) { codec_client_->close(); } else { - parent().onConnectionDraining(codec_client_->connection()); parent_.transitionActiveClientState(*this, ActiveClient::State::Draining); } } @@ -203,6 +184,9 @@ void MultiplexedActiveClientBase::onStreamReset(Http::StreamResetReason reason) case StreamResetReason::RemoteReset: parent_.host()->cluster().trafficStats()->upstream_rq_rx_reset_.inc(); break; + case StreamResetReason::RemoteResetNoError: + parent_.host()->cluster().trafficStats()->upstream_rq_rx_reset_no_error_.inc(); + break; case StreamResetReason::LocalRefusedStreamReset: case StreamResetReason::RemoteRefusedStreamReset: case StreamResetReason::Overflow: @@ -241,17 +225,5 @@ MultiplexedActiveClientBase::newStreamEncoder(ResponseDecoderHandlePtr response_ return codec_client_->newStream(std::move(response_decoder_handle)); } -void MultiplexedActiveClientBase::onEvent(Network::ConnectionEvent event) { - if (event == Network::ConnectionEvent::Connected || - event == Network::ConnectionEvent::ConnectedZeroRtt) { - parent().onConnectionOpen(codec_client_->connection()); - } else if (event == Network::ConnectionEvent::LocalClose || - event == Network::ConnectionEvent::RemoteClose) { - parent().onConnectionDraining(codec_client_->connection()); - } - - ActiveClient::onEvent(event); -} - } // namespace Http } // namespace Envoy diff --git a/source/common/http/conn_pool_base.h b/source/common/http/conn_pool_base.h index 4cf74e1e6a7d0..afbf10b30922b 100644 --- a/source/common/http/conn_pool_base.h +++ b/source/common/http/conn_pool_base.h @@ -74,6 +74,9 @@ class HttpConnPoolImplBase : public Envoy::ConnectionPool::ConnPoolImplBase, drainConnectionsImpl(drain_behavior); } Upstream::HostDescriptionConstSharedPtr host() const override { return host_; } + const Network::ConnectionSocket::OptionsSharedPtr& socketOptions() override { + return Envoy::ConnectionPool::ConnPoolImplBase::socketOptions(); + } ConnectionPool::Cancellable* newStream(Http::ResponseDecoder& response_decoder, Http::ConnectionPool::Callbacks& callbacks, const Instance::StreamOptions& options) override; @@ -95,27 +98,18 @@ class HttpConnPoolImplBase : public Envoy::ConnectionPool::ConnPoolImplBase, virtual CodecClientPtr createCodecClient(Upstream::Host::CreateConnectionData& data) PURE; Random::RandomGenerator& randomGenerator() { return random_generator_; } - virtual absl::optional& origin() { return origin_; } + virtual std::optional& origin() { return origin_; } virtual Http::HttpServerPropertiesCacheSharedPtr cache() { return nullptr; } - void setLifetimeCallbacks(OptRef callbacks, - std::vector hash_key) override; - - void onConnectionOpen(const Network::Connection& connection); - - void onConnectionDraining(const Network::Connection& connection); - protected: friend class ActiveClient; - void setOrigin(absl::optional origin) { origin_ = origin; } + void setOrigin(std::optional origin) { origin_ = origin; } Random::RandomGenerator& random_generator_; private: - absl::optional origin_; - OptRef callbacks_; - std::vector hash_key_; + std::optional origin_; }; // An implementation of Envoy::ConnectionPool::ActiveClient for HTTP/1.1 and HTTP/2 @@ -152,7 +146,7 @@ class ActiveClient : public Envoy::ConnectionPool::ActiveClient { } void initializeReadFilters() override { codec_client_->initializeReadFilters(); } - absl::optional protocol() const override { return codec_client_->protocol(); } + std::optional protocol() const override { return codec_client_->protocol(); } void close(Network::ConnectionCloseType type, absl::string_view details) override { codec_client_->close(type, details); } @@ -193,7 +187,7 @@ class FixedHttpConnPoolImpl : public HttpConnPoolImplBase { Random::RandomGenerator& random_generator, Upstream::ClusterConnectivityState& state, CreateClientFn client_fn, CreateCodecFn codec_fn, std::vector protocols, Server::OverloadManager& overload_manager, - absl::optional origin = absl::nullopt, + std::optional origin = std::nullopt, Http::HttpServerPropertiesCacheSharedPtr cache = nullptr) : HttpConnPoolImplBase(host, priority, dispatcher, options, transport_socket_options, random_generator, state, protocols, overload_manager), @@ -251,9 +245,6 @@ class MultiplexedActiveClientBase : public CodecClientCallbacks, void onGoAway(Http::GoAwayErrorCode error_code) override; void onSettings(ReceivedSettings& settings) override; - // Override to provide the lifetimeCallbacks. - void onEvent(Network::ConnectionEvent event) override; - private: bool closed_with_active_rq_{}; }; diff --git a/source/common/http/conn_pool_grid.cc b/source/common/http/conn_pool_grid.cc index fc30d1057c0c7..8b1f4a5306cf2 100644 --- a/source/common/http/conn_pool_grid.cc +++ b/source/common/http/conn_pool_grid.cc @@ -136,6 +136,12 @@ bool ConnectivityGrid::WrapperCallbacks::shouldAttemptSecondHttp3Connection() { void ConnectivityGrid::WrapperCallbacks::onConnectionAttemptFailed( ConnectionAttemptCallbacks* attempt, ConnectionPool::PoolFailureReason reason, absl::string_view transport_failure_reason, Upstream::HostDescriptionConstSharedPtr host) { + if (delete_started_ && Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.conn_pool_grid_early_return_on_teardown")) { + ENVOY_LOG(trace, "Uninteresting connection attempt to host {} failed.", + host != nullptr ? host->hostname() : "unknown"); + return; + } ENVOY_LOG(trace, "{} pool failed to create connection to host '{}'.", describePool(attempt->pool()), host->hostname()); grid_.dispatcher_.deferredDelete(attempt->removeFromList(connection_attempts_)); @@ -210,6 +216,11 @@ ConnectivityGrid::StreamCreationResult ConnectivityGrid::WrapperCallbacks::newStream(ConnectionPool::Instance& pool) { ENVOY_LOG(trace, "{} pool attempting to create a new stream to host '{}'.", describePool(pool), grid_.origin_.hostname_); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.connectivity_grid_prevent_double_h2_scheduled") && + grid_.http2_pool_ != nullptr && &pool == grid_.http2_pool_.get()) { + has_attempted_http2_ = true; + } auto attempt = std::make_unique(*this, pool); LinkedList::moveIntoList(std::move(attempt), connection_attempts_); if (!next_attempt_timer_->enabled()) { @@ -222,7 +233,7 @@ ConnectivityGrid::WrapperCallbacks::newStream(ConnectionPool::Instance& pool) { void ConnectivityGrid::WrapperCallbacks::onConnectionAttemptReady( ConnectionAttemptCallbacks* attempt, RequestEncoder& encoder, Upstream::HostDescriptionConstSharedPtr host, StreamInfo::StreamInfo& info, - absl::optional protocol) { + std::optional protocol) { ENVOY_LOG(trace, "{} pool successfully connected to host '{}'.", describePool(attempt->pool()), host->hostname()); if (!grid_.isPoolHttp3(attempt->pool())) { @@ -258,7 +269,7 @@ void ConnectivityGrid::WrapperCallbacks::maybeMarkHttp3Broken() { void ConnectivityGrid::WrapperCallbacks::ConnectionAttemptCallbacks::onPoolReady( RequestEncoder& encoder, Upstream::HostDescriptionConstSharedPtr host, - StreamInfo::StreamInfo& info, absl::optional protocol) { + StreamInfo::StreamInfo& info, std::optional protocol) { cancellable_ = nullptr; // Attempt succeeded and can no longer be cancelled. parent_.onConnectionAttemptReady(this, encoder, host, info, protocol); } @@ -294,7 +305,7 @@ void ConnectivityGrid::WrapperCallbacks::onNextAttemptTimer() { attemptSecondHttp3Connection(); } } -absl::optional +std::optional ConnectivityGrid::WrapperCallbacks::tryAnotherConnection() { if (grid_.destroying_) { return {}; @@ -417,7 +428,6 @@ ConnectionPool::InstancePtr ConnectivityGrid::createHttp3Pool(bool attempt_alter void ConnectivityGrid::setupPool(ConnectionPool::Instance& pool) { pool.addIdleCallback([this]() { onIdleReceived(); }); - pool.setLifetimeCallbacks(makeOptRefFromPtr(this), hash_key_); } bool ConnectivityGrid::hasActiveConnections() const { @@ -611,25 +621,5 @@ void ConnectivityGrid::onZeroRttHandshakeFailed() { getHttp3StatusTracker().markHttp3FailedRecently(); } -void ConnectivityGrid::onConnectionOpen(ConnectionPool::Instance&, std::vector&, - const Network::Connection& connection) { - if (callbacks_.has_value()) { - callbacks_->onConnectionOpen(*this, hash_key_, connection); - } -} - -void ConnectivityGrid::onConnectionDraining(ConnectionPool::Instance&, std::vector&, - const Network::Connection& connection) { - if (callbacks_.has_value()) { - callbacks_->onConnectionDraining(*this, hash_key_, connection); - } -} - -void ConnectivityGrid::setLifetimeCallbacks( - OptRef callbacks, std::vector hash_key) { - callbacks_ = callbacks; - hash_key_ = std::move(hash_key); -} - } // namespace Http } // namespace Envoy diff --git a/source/common/http/conn_pool_grid.h b/source/common/http/conn_pool_grid.h index 76fd1f90e37a2..6a47a5763ecc0 100644 --- a/source/common/http/conn_pool_grid.h +++ b/source/common/http/conn_pool_grid.h @@ -30,7 +30,6 @@ namespace Http { // the host's address list. class ConnectivityGrid : public ConnectionPool::Instance, public Http3::PoolConnectResultCallback, - public ConnectionPool::ConnectionLifetimeCallbacks, protected Logger::Loggable { public: struct ConnectivityOptions { @@ -79,7 +78,7 @@ class ConnectivityGrid : public ConnectionPool::Instance, Upstream::HostDescriptionConstSharedPtr host) override; void onPoolReady(RequestEncoder& encoder, Upstream::HostDescriptionConstSharedPtr host, StreamInfo::StreamInfo& info, - absl::optional protocol) override; + std::optional protocol) override; ConnectionPool::Instance& pool() { return pool_; } @@ -104,7 +103,7 @@ class ConnectivityGrid : public ConnectionPool::Instance, // Called on pool failure or timeout to kick off another connection attempt. // Returns the StreamCreationResult if there is a failover pool and a // connection has been attempted, an empty optional otherwise. - absl::optional tryAnotherConnection(); + std::optional tryAnotherConnection(); // This timer is registered when an initial HTTP/3 attempt is started. // The timeout for TCP failover and HTTP/3 happy eyeballs are the same, so @@ -122,7 +121,7 @@ class ConnectivityGrid : public ConnectionPool::Instance, void onConnectionAttemptReady(ConnectionAttemptCallbacks* attempt, RequestEncoder& encoder, Upstream::HostDescriptionConstSharedPtr host, StreamInfo::StreamInfo& info, - absl::optional protocol); + std::optional protocol); // Called by onConnectionAttemptFailed and on grid deletion destruction to let wrapper // callback subscribers know the connect attempt failed. @@ -177,7 +176,7 @@ class ConnectivityGrid : public ConnectionPool::Instance, bool tcp_attempt_succeeded_{}; // Latch the passed-in stream options. const Instance::StreamOptions stream_options_{}; - absl::optional prev_pool_failure_reason_; + std::optional prev_pool_failure_reason_; std::string prev_pool_transport_failure_reason_; bool delete_started_ = false; }; @@ -207,6 +206,7 @@ class ConnectivityGrid : public ConnectionPool::Instance, bool isIdle() const override; void drainConnections(Envoy::ConnectionPool::DrainBehavior drain_behavior) override; Upstream::HostDescriptionConstSharedPtr host() const override; + const Network::ConnectionSocket::OptionsSharedPtr& socketOptions() override { return options_; } bool maybePreconnect(float preconnect_ratio) override; absl::string_view protocolDescription() const override { return "connection grid"; } @@ -229,15 +229,6 @@ class ConnectivityGrid : public ConnectionPool::Instance, void onHandshakeComplete() override; void onZeroRttHandshakeFailed() override; - // ConnectionPool::ConnectionLifetimeCallbacks - void onConnectionOpen(ConnectionPool::Instance& pool, std::vector& hash_key, - const Network::Connection& connection) override; - void onConnectionDraining(ConnectionPool::Instance& pool, std::vector& hash_key, - const Network::Connection& connection) override; - - void setLifetimeCallbacks(OptRef callbacks, - std::vector hash_key) override; - protected: // Set the required idle callback on the pool. void setupPool(ConnectionPool::Instance& pool); @@ -318,9 +309,6 @@ class ConnectivityGrid : public ConnectionPool::Instance, bool deferred_deleting_{}; OptRef network_observer_registry_; - - OptRef callbacks_; - std::vector hash_key_; }; } // namespace Http diff --git a/source/common/http/filter_chain_helper.h b/source/common/http/filter_chain_helper.h index 62cb678bca10a..668dc822ffbf9 100644 --- a/source/common/http/filter_chain_helper.h +++ b/source/common/http/filter_chain_helper.h @@ -32,7 +32,7 @@ class MissingConfigFilter : public Http::PassThroughDecoderFilter { decoder_callbacks_->streamInfo().setResponseFlag( StreamInfo::CoreResponseFlag::NoFilterConfigFound); decoder_callbacks_->sendLocalReply(Http::Code::InternalServerError, EMPTY_STRING, nullptr, - absl::nullopt, EMPTY_STRING); + std::nullopt, EMPTY_STRING); return Http::FilterHeadersStatus::StopIteration; } }; diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index 67b746f49fab6..7635d8bbe55bd 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -67,7 +67,7 @@ void ActiveStreamFilterBase::commonContinue() { // Set ScopeTrackerScopeState if there's no existing crash context. ScopeTrackedObjectStack encapsulated_object; - absl::optional state; + std::optional state; if (parent_.dispatcher_.trackedObjectStackIsEmpty()) { restoreContextOnContinue(encapsulated_object); state.emplace(&encapsulated_object, parent_.dispatcher_); @@ -376,12 +376,11 @@ uint64_t ActiveStreamFilterBase::bufferLimit() { return parent_.buffer_limit_; } void ActiveStreamFilterBase::sendLocalReply( Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { if (!streamInfo().filterState()->hasData(LocalReplyFilterStateKey)) { streamInfo().filterState()->setData( LocalReplyFilterStateKey, std::make_shared(filter_context_.config_name), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::FilterChain); } @@ -490,6 +489,10 @@ void ActiveStreamDecoderFilter::injectDecodedDataToFilterChain(Buffer::Instance& FilterManager::FilterIterationStartState::CanStartFromCurrent); } +OptRef ActiveStreamDecoderFilter::webTransportSession() { + return parent_.webTransportSession(); +} + void ActiveStreamDecoderFilter::continueDecoding() { commonContinue(); } const Buffer::Instance* ActiveStreamDecoderFilter::decodingBuffer() { return parent_.buffered_request_data_.get(); @@ -506,7 +509,7 @@ void ActiveStreamDecoderFilter::modifyDecodingBuffer( void ActiveStreamDecoderFilter::sendLocalReply( Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { ActiveStreamFilterBase::sendLocalReply(code, body, modify_headers, grpc_status, details); } @@ -570,7 +573,7 @@ void ActiveStreamDecoderFilter::requestDataTooLarge() { } else { parent_.filter_manager_callbacks_.onRequestDataTooLarge(); sendLocalReply(Code::PayloadTooLarge, CodeUtility::toString(Code::PayloadTooLarge), nullptr, - absl::nullopt, StreamInfo::ResponseCodeDetails::get().RequestPayloadTooLarge); + std::nullopt, StreamInfo::ResponseCodeDetails::get().RequestPayloadTooLarge); } } @@ -843,7 +846,7 @@ void FilterManager::addDecodedData(ActiveStreamDecoderFilter& filter, Buffer::In decodeData(&filter, data, false, FilterIterationStartState::AlwaysStartFromNext); } else { IS_ENVOY_BUG("Invalid request data"); - sendLocalReply(Http::Code::BadGateway, "Filter error", nullptr, absl::nullopt, + sendLocalReply(Http::Code::BadGateway, "Filter error", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().FilterAddedInvalidRequestData); } } @@ -1023,7 +1026,7 @@ void DownstreamFilterManager::onLocalReply(StreamFilterBase::LocalReplyData& dat void DownstreamFilterManager::sendLocalReply( Code code, absl::string_view body, const std::function& modify_headers, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { ASSERT(!state_.under_on_local_reply_); const bool is_head_request = state_.is_head_request_; const bool is_grpc_request = state_.is_grpc_request_; @@ -1102,7 +1105,7 @@ void DownstreamFilterManager::sendLocalReply( void DownstreamFilterManager::prepareLocalReplyViaFilterChain( bool is_grpc_request, Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { ENVOY_STREAM_LOG(debug, "Preparing local reply with details {}", *this, details); ASSERT(!filter_manager_callbacks_.responseHeaders().has_value()); // For early error handling, do a best-effort attempt to create a filter chain @@ -1154,7 +1157,7 @@ FilterManager::CreateChainResult DownstreamFilterManager::createDownstreamFilter void DownstreamFilterManager::sendLocalReplyViaFilterChain( bool is_grpc_request, Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { ENVOY_STREAM_LOG(debug, "Sending local reply with details {}", *this, details); ASSERT(!filter_manager_callbacks_.responseHeaders().has_value()); // For early error handling, do a best-effort attempt to create a filter chain @@ -1190,7 +1193,7 @@ void DownstreamFilterManager::sendLocalReplyViaFilterChain( void DownstreamFilterManager::sendDirectLocalReply( Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status) { + const std::optional grpc_status) { // Make sure we won't end up with nested watermark calls from the body buffer. state_.encoder_filters_streaming_ = true; Http::Utility::sendLocalReply( @@ -1354,7 +1357,7 @@ void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHea if (!status.ok()) { // If the check failed, then we reply with BadGateway, and stop the further processing. sendLocalReply( - Http::Code::BadGateway, status.message(), nullptr, absl::nullopt, + Http::Code::BadGateway, status.message(), nullptr, std::nullopt, absl::StrCat(StreamInfo::ResponseCodeDetails::get().FilterRemovedRequiredResponseHeaders, "{", StringUtil::replaceAllEmptySpace(status.message()), "}")); return; @@ -1453,7 +1456,7 @@ void FilterManager::addEncodedData(ActiveStreamEncoderFilter& filter, Buffer::In encodeData(&filter, data, false, FilterIterationStartState::AlwaysStartFromNext); } else { IS_ENVOY_BUG("Invalid response data"); - sendLocalReply(Http::Code::BadGateway, "Filter error", nullptr, absl::nullopt, + sendLocalReply(Http::Code::BadGateway, "Filter error", nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().FilterAddedInvalidResponseData); } } @@ -1944,7 +1947,7 @@ void ActiveStreamEncoderFilter::modifyEncodingBuffer( void ActiveStreamEncoderFilter::sendLocalReply( Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, absl::string_view details) { + const std::optional grpc_status, absl::string_view details) { ActiveStreamFilterBase::sendLocalReply(code, body, modify_headers, grpc_status, details); } @@ -1960,7 +1963,7 @@ void ActiveStreamEncoderFilter::responseDataTooLarge() { // reset the stream. parent_.sendLocalReply( Http::Code::InternalServerError, CodeUtility::toString(Http::Code::InternalServerError), - nullptr, absl::nullopt, StreamInfo::ResponseCodeDetails::get().ResponsePayloadTooLarge); + nullptr, std::nullopt, StreamInfo::ResponseCodeDetails::get().ResponsePayloadTooLarge); } } diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index a8670d67ee166..a38ce58356f60 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -44,19 +44,23 @@ constexpr absl::string_view LocalReplyFilterStateKey = "envoy.filters.network.http_connection_manager.local_reply_owner"; class LocalReplyOwnerObject : public StreamInfo::FilterState::Object { public: - LocalReplyOwnerObject(const std::string& filter_config_name) + // The filter_config_name is expected to outlive the LocalReplyOwnerObject, as it typically + // comes from the filter configuration. + LocalReplyOwnerObject(absl::string_view filter_config_name) : filter_config_name_(filter_config_name) {} ProtobufTypes::MessagePtr serializeAsProto() const override { auto message = std::make_unique(); - message->set_value(filter_config_name_); + message->set_value(std::string(filter_config_name_)); return message; } - absl::optional serializeAsString() const override { return filter_config_name_; } + std::optional serializeAsString() const override { + return std::string(filter_config_name_); + } private: - const std::string filter_config_name_; + const absl::string_view filter_config_name_; }; // TODO(wbpcode): Rather than allocating every filter with an unique pointer, we could @@ -201,7 +205,7 @@ struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, void sendLocalReply(Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details); // A vector to save metadata when the current filter's [de|en]codeMetadata() can not be called, @@ -270,6 +274,7 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, void handleMetadataAfterHeadersCallback() override; // Http::StreamDecoderFilterCallbacks + OptRef webTransportSession() override; void addDecodedData(Buffer::Instance& data, bool streaming) override; void injectDecodedDataToFilterChain(Buffer::Instance& data, bool end_stream) override; RequestTrailerMap& addDecodedTrailers() override; @@ -281,7 +286,7 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, void sendLocalReply(Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) override; void encode1xxHeaders(ResponseHeaderMapPtr&& headers) override; void encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, @@ -370,7 +375,7 @@ struct ActiveStreamEncoderFilter : public ActiveStreamFilterBase, void modifyEncodingBuffer(std::function callback) override; void sendLocalReply(Code code, absl::string_view body, std::function modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) override; void responseDataTooLarge(); @@ -388,6 +393,13 @@ class FilterManagerCallbacks { public: virtual ~FilterManagerCallbacks() = default; + /** + * @return the WebTransport session of the codec stream this filter manager is bound to, if any. + * The downstream HTTP connection manager returns the downstream codec stream's session; the + * default empty OptRef applies elsewhere. + */ + virtual OptRef webTransportSession() { return {}; } + /** * Called when the provided headers have been encoded by all the filters in the chain. * @param response_headers the encoded headers. @@ -643,10 +655,10 @@ class OverridableRemoteConnectionInfoSetterStreamInfo : public StreamInfo::Strea const std::vector& requestedApplicationProtocols() const override { return StreamInfoImpl::downstreamAddressProvider().requestedApplicationProtocols(); } - absl::optional connectionID() const override { + std::optional connectionID() const override { return StreamInfoImpl::downstreamAddressProvider().connectionID(); } - absl::optional interfaceName() const override { + std::optional interfaceName() const override { return StreamInfoImpl::downstreamAddressProvider().interfaceName(); } Ssl::ConnectionInfoConstSharedPtr sslConnection() const override { @@ -667,7 +679,7 @@ class OverridableRemoteConnectionInfoSetterStreamInfo : public StreamInfo::Strea absl::string_view ja4Hash() const override { return StreamInfoImpl::downstreamAddressProvider().ja4Hash(); } - const absl::optional& roundTripTime() const override { + const std::optional& roundTripTime() const override { return StreamInfoImpl::downstreamAddressProvider().roundTripTime(); } OptRef filterChainInfo() const override { @@ -700,6 +712,12 @@ class FilterManager : public ScopeTrackedObject, Logger::Loggable webTransportSession() { + return filter_manager_callbacks_.webTransportSession(); + } + // ScopeTrackedObject OptRef trackedStream() const override { return streamInfo(); } void dumpState(std::ostream& os, int indent_level = 0) const override { @@ -793,7 +811,7 @@ class FilterManager : public ScopeTrackedObject, Logger::Loggable& modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) PURE; void resetStream(StreamResetReason reason, absl::string_view transport_failure_reason); @@ -1027,8 +1045,8 @@ class FilterManager : public ScopeTrackedObject, Logger::Loggable route() const override { return route_; } - absl::optional filterDisabled(absl::string_view config_name) const override { - return route_ ? route_->filterDisabled(config_name) : absl::nullopt; + std::optional filterDisabled(absl::string_view config_name) const override { + return route_ ? route_->filterDisabled(config_name) : std::nullopt; } const StreamInfo::StreamInfo& streamInfo() const override { return manager_.streamInfo(); } @@ -1233,7 +1251,7 @@ class DownstreamFilterManager : public FilterManager { void sendLocalReply(Code code, absl::string_view body, const std::function& modify_headers, - const absl::optional grpc_status, + const std::optional grpc_status, absl::string_view details) override; /** @@ -1265,7 +1283,7 @@ class DownstreamFilterManager : public FilterManager { void sendLocalReplyViaFilterChain( bool is_grpc_request, Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status, absl::string_view details); + const std::optional grpc_status, absl::string_view details); /** * Prepares a local reply that will be sent along the encoder filters in @@ -1274,7 +1292,7 @@ class DownstreamFilterManager : public FilterManager { void prepareLocalReplyViaFilterChain( bool is_grpc_request, Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status, absl::string_view details); + const std::optional grpc_status, absl::string_view details); /** * Executes a prepared local reply along the encoder filters. @@ -1288,7 +1306,7 @@ class DownstreamFilterManager : public FilterManager { void sendDirectLocalReply(Code code, absl::string_view body, const std::function& modify_headers, bool is_head_request, - const absl::optional grpc_status); + const std::optional grpc_status); private: OverridableRemoteConnectionInfoSetterStreamInfo stream_info_; diff --git a/source/common/http/hash_policy.cc b/source/common/http/hash_policy.cc index c7810500eba78..5d9bfd637e12a 100644 --- a/source/common/http/hash_policy.cc +++ b/source/common/http/hash_policy.cc @@ -40,16 +40,16 @@ class HeaderHashMethod : public HashMethodImplBase { } } - absl::optional evaluate(OptRef headers, - OptRef, - HashPolicy::AddCookieCallback) const override { + std::optional evaluate(OptRef headers, + OptRef, + HashPolicy::AddCookieCallback) const override { if (!headers.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto header = headers->get(header_name_); if (header.empty()) { - return absl::nullopt; + return std::nullopt; } absl::InlinedVector header_values; @@ -87,19 +87,19 @@ class CookieHashMethod : public HashMethodImplBase { CookieHashMethod(const envoy::config::route::v3::RouteAction::HashPolicy::Cookie& cookie, bool terminal) : HashMethodImplBase(terminal), name_(cookie.name()), path_(cookie.path()), - ttl_(cookie.has_ttl() ? absl::optional(cookie.ttl().seconds()) - : absl::nullopt) { + ttl_(cookie.has_ttl() ? std::optional(cookie.ttl().seconds()) + : std::nullopt) { attributes_.reserve(cookie.attributes().size()); for (const auto& attribute : cookie.attributes()) { attributes_.push_back(CookieAttribute{attribute.name(), attribute.value()}); } } - absl::optional evaluate(OptRef headers, - OptRef, - HashPolicy::AddCookieCallback add_cookie) const override { + std::optional evaluate(OptRef headers, + OptRef, + HashPolicy::AddCookieCallback add_cookie) const override { if (!headers.has_value()) { - return absl::nullopt; + return std::nullopt; } const std::string exist_value = Utility::parseCookieValue(*headers, name_); @@ -113,18 +113,18 @@ class CookieHashMethod : public HashMethodImplBase { // 1. The cookie has no TTL. // 2. The cookie generation callback is null. if (!ttl_.has_value() || add_cookie == nullptr) { - return absl::nullopt; + return std::nullopt; } const std::string new_value = add_cookie(name_, path_, ttl_.value(), attributes_); - return new_value.empty() ? absl::nullopt - : absl::optional(HashUtil::xxHash64(new_value)); + return new_value.empty() ? std::nullopt + : std::optional(HashUtil::xxHash64(new_value)); } private: const std::string name_; const std::string path_; - const absl::optional ttl_; + const std::optional ttl_; std::vector attributes_; }; @@ -132,26 +132,26 @@ class IpHashMethod : public HashMethodImplBase { public: IpHashMethod(bool terminal) : HashMethodImplBase(terminal) {} - absl::optional evaluate(OptRef, - OptRef info, - HashPolicy::AddCookieCallback) const override { + std::optional evaluate(OptRef, + OptRef info, + HashPolicy::AddCookieCallback) const override { if (!info.has_value()) { - return absl::nullopt; + return std::nullopt; } const auto& conn = info->downstreamAddressProvider(); const auto& downstream_addr = conn.remoteAddress(); if (downstream_addr == nullptr) { - return absl::nullopt; + return std::nullopt; } auto* downstream_ip = downstream_addr->ip(); if (downstream_ip == nullptr) { - return absl::nullopt; + return std::nullopt; } const auto& downstream_addr_str = downstream_ip->addressAsString(); if (downstream_addr_str.empty()) { - return absl::nullopt; + return std::nullopt; } return HashUtil::xxHash64(downstream_addr_str); } @@ -162,11 +162,11 @@ class QueryParameterHashMethod : public HashMethodImplBase { QueryParameterHashMethod(const std::string& parameter_name, bool terminal) : HashMethodImplBase(terminal), parameter_name_(parameter_name) {} - absl::optional evaluate(OptRef headers, - OptRef, - HashPolicy::AddCookieCallback) const override { + std::optional evaluate(OptRef headers, + OptRef, + HashPolicy::AddCookieCallback) const override { if (!headers.has_value()) { - return absl::nullopt; + return std::nullopt; } const Utility::QueryParamsMulti query_parameters = @@ -175,7 +175,7 @@ class QueryParameterHashMethod : public HashMethodImplBase { if (val.has_value()) { return HashUtil::xxHash64(val.value()); } - return absl::nullopt; + return std::nullopt; } private: @@ -187,15 +187,15 @@ class FilterStateHashMethod : public HashMethodImplBase { FilterStateHashMethod(const std::string& key, bool terminal) : HashMethodImplBase(terminal), key_(key) {} - absl::optional evaluate(OptRef, - OptRef info, - HashPolicy::AddCookieCallback) const override { + std::optional evaluate(OptRef, + OptRef info, + HashPolicy::AddCookieCallback) const override { if (!info.has_value()) { - return absl::nullopt; + return std::nullopt; } auto filter_state = info->filterState().getDataReadOnly(key_); - return filter_state != nullptr ? filter_state->hash() : absl::nullopt; + return filter_state != nullptr ? filter_state->hash() : std::nullopt; } private: @@ -252,13 +252,13 @@ HashPolicyImpl::HashPolicyImpl( } } -absl::optional +std::optional HashPolicyImpl::generateHash(OptRef headers, OptRef info, HashPolicy::AddCookieCallback add_cookie) const { - absl::optional hash; + std::optional hash; for (const HashMethodPtr& hash_impl : hash_impls_) { - const absl::optional new_hash = hash_impl->evaluate(headers, info, add_cookie); + const std::optional new_hash = hash_impl->evaluate(headers, info, add_cookie); if (new_hash) { // Rotating the old value prevents duplicate hash rules from cancelling each other out // and preserves all of the entropy diff --git a/source/common/http/hash_policy.h b/source/common/http/hash_policy.h index 3d739b8936bb2..8d1dd50af19e9 100644 --- a/source/common/http/hash_policy.h +++ b/source/common/http/hash_policy.h @@ -22,16 +22,16 @@ class HashPolicyImpl : public HashPolicy { Regex::Engine& regex_engine); // Http::HashPolicy - absl::optional generateHash(OptRef headers, - OptRef info, - AddCookieCallback add_cookie = nullptr) const override; + std::optional generateHash(OptRef headers, + OptRef info, + AddCookieCallback add_cookie = nullptr) const override; class HashMethod { public: virtual ~HashMethod() = default; - virtual absl::optional evaluate(OptRef headers, - OptRef info, - AddCookieCallback add_cookie = nullptr) const PURE; + virtual std::optional evaluate(OptRef headers, + OptRef info, + AddCookieCallback add_cookie = nullptr) const PURE; // If the method is a terminal method, ignore rest of the hash policy chain. virtual bool terminal() const PURE; diff --git a/source/common/http/header_map_impl.cc b/source/common/http/header_map_impl.cc index 1a5b4e6886a1a..3cec5b6cbc15c 100644 --- a/source/common/http/header_map_impl.cc +++ b/source/common/http/header_map_impl.cc @@ -394,6 +394,7 @@ void HeaderMapImpl::iterate(HeaderMap::ConstIterateCb cb) const { } void HeaderMapImpl::iterateReverse(HeaderMap::ConstIterateCb cb) const { + // NOLINTNEXTLINE(modernize-loop-convert) for (auto it = headers_.rbegin(); it != headers_.rend(); it++) { if (cb(*it) == HeaderMap::Iterate::Break) { break; diff --git a/source/common/http/header_map_impl.h b/source/common/http/header_map_impl.h index f053e42cacbd5..176f2b2673410 100644 --- a/source/common/http/header_map_impl.h +++ b/source/common/http/header_map_impl.h @@ -174,13 +174,13 @@ class HeaderMapImpl : NonCopyable { return ConstSingleton::get().size_; } - static absl::optional lookup(HeaderMapImpl& header_map, - absl::string_view key) { + static std::optional lookup(HeaderMapImpl& header_map, + absl::string_view key) { const auto& entry = ConstSingleton::get().find(key); if (entry != nullptr) { return entry(header_map); } else { - return absl::nullopt; + return std::nullopt; } } @@ -335,7 +335,7 @@ class HeaderMapImpl : NonCopyable { void updateSize(uint64_t from_size, uint64_t to_size); void addSize(uint64_t size); void subtractSize(uint64_t size); - virtual absl::optional staticLookup(absl::string_view) PURE; + virtual std::optional staticLookup(absl::string_view) PURE; virtual void clearInline() PURE; virtual HeaderEntryImpl** inlineHeaders() PURE; @@ -472,7 +472,7 @@ template class TypedHeaderMapImpl : public HeaderMapImpl, publ } protected: - absl::optional staticLookup(absl::string_view key) override { + std::optional staticLookup(absl::string_view key) override { return StaticLookupTable::lookup(*this, key); } virtual const HeaderEntryImpl* const* constInlineHeaders() const PURE; diff --git a/source/common/http/header_mutation.h b/source/common/http/header_mutation.h index eeabd3e0b38a8..25d03110f4830 100644 --- a/source/common/http/header_mutation.h +++ b/source/common/http/header_mutation.h @@ -1,7 +1,7 @@ #pragma once #include "envoy/config/common/mutation_rules/v3/mutation_rules.pb.h" -#include "envoy/formatter/substitution_formatter_base.h" +#include "envoy/formatter/substitution_formatter.h" #include "envoy/http/header_evaluator.h" #include "envoy/server/factory_context.h" diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index f18bcbe5f07bc..304bdfac63192 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -37,18 +37,49 @@ using SharedResponseCodeDetails = ConstSingleton& config_headers) { - // No headers to match is considered a match. - if (!config_headers.empty()) { + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.match_headers_individually")) { for (const HeaderDataPtr& cfg_header_data : config_headers) { if (!cfg_header_data->matchesHeaders(request_headers)) { return false; } } + return true; + } + for (const HeaderDataPtr& cfg_header_data : config_headers) { + if (!cfg_header_data->matchesHeadersIndividually(request_headers)) { + return false; + } } - return true; } +template +static bool matchAnyHeaderImpl(const HeaderMap& request_headers, + const std::vector& config_headers) { + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.match_headers_individually")) { + for (const PointerType& cfg_header_data : config_headers) { + if (cfg_header_data->matchesHeaders(request_headers)) { + return true; + } + } + return false; + } + for (const PointerType& cfg_header_data : config_headers) { + if (cfg_header_data->matchesHeadersIndividually(request_headers)) { + return true; + } + } + return false; +} +bool HeaderUtility::matchAnyHeader(const HeaderMap& request_headers, + const std::vector& config_headers) { + return matchAnyHeaderImpl(request_headers, config_headers); +} +bool HeaderUtility::matchAnyHeader(const HeaderMap& request_headers, + const std::vector& config_headers) { + return matchAnyHeaderImpl(request_headers, config_headers); +} + HeaderUtility::GetAllOfHeaderAsStringResult HeaderUtility::getAllOfHeaderAsString(const HeaderMap::GetResult& header_value, absl::string_view separator) { @@ -144,7 +175,7 @@ bool HeaderUtility::headerNameIsValid(absl::string_view header_key) { // TODO(yanavlasov): make validation in HTTP/2 case stricter. bool is_valid = true; for (auto iter = header_key.begin(); iter != header_key.end() && is_valid; ++iter) { - is_valid &= testCharInTable(kGenericHeaderNameCharTable, *iter); + is_valid &= CharTables::kGenericHeaderName.hasChar(*iter); } return is_valid; } @@ -320,7 +351,7 @@ bool HeaderUtility::isCapsuleProtocol(const RequestOrResponseHeaderMap& headers) return false; } // Parses the header value and extracts the boolean value ignoring parameters. - absl::optional header_item = + std::optional header_item = quiche::structured_headers::ParseItem(capsule_protocol[0]->value().getStringView()); return header_item && header_item->item.is_boolean() && header_item->item.GetBoolean(); } @@ -373,22 +404,22 @@ bool HeaderUtility::hostHasPort(absl::string_view original_host) { return true; } -absl::optional HeaderUtility::stripPortFromHost(RequestHeaderMap& headers, - absl::optional listener_port) { +std::optional HeaderUtility::stripPortFromHost(RequestHeaderMap& headers, + std::optional listener_port) { const absl::string_view original_host = headers.getHostValue(); const absl::string_view::size_type port_start = getPortStart(original_host); if (port_start == absl::string_view::npos) { - return absl::nullopt; + return std::nullopt; } const absl::string_view port_str = original_host.substr(port_start + 1); uint32_t port = 0; if (!absl::SimpleAtoi(port_str, &port)) { - return absl::nullopt; + return std::nullopt; } if (listener_port.has_value() && port != listener_port) { // We would strip ports only if it is specified and they are the same, as local port of the // listener. - return absl::nullopt; + return std::nullopt; } const absl::string_view host = original_host.substr(0, port_start); headers.setHost(host); @@ -434,7 +465,7 @@ constexpr bool isInvalidToken(unsigned char c) { return true; } -absl::optional> +std::optional> HeaderUtility::requestHeadersValid(const RequestHeaderMap& headers) { // Make sure the host is valid. if (headers.Host() && !HeaderUtility::authorityIsValid(headers.Host()->value().getStringView())) { @@ -449,7 +480,7 @@ HeaderUtility::requestHeadersValid(const RequestHeaderMap& headers) { if (headers.Scheme() && absl::StrContains(headers.Scheme()->value().getStringView(), ",")) { return SharedResponseCodeDetails::get().InvalidScheme; } - return absl::nullopt; + return std::nullopt; } bool HeaderUtility::shouldCloseConnection(Http::Protocol protocol, @@ -549,7 +580,7 @@ Http::Status HeaderUtility::checkValidRequestHeaders(const Http::RequestHeaderMa } Http::Status HeaderUtility::checkRequiredResponseHeaders(const Http::ResponseHeaderMap& headers) { - const absl::optional status = Utility::getResponseStatusOrNullopt(headers); + const std::optional status = Utility::getResponseStatusOrNullopt(headers); if (!status.has_value()) { return absl::InvalidArgumentError( absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Status.get())); @@ -593,7 +624,7 @@ HeaderUtility::validateContentLength(absl::string_view header_value, bool& should_close_connection, size_t& content_length_output) { should_close_connection = false; std::vector values = absl::StrSplit(header_value, ','); - absl::optional content_length; + std::optional content_length; for (const absl::string_view& value : values) { uint64_t new_value; if (!absl::SimpleAtoi(value, &new_value) || diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index 095cb4a191643..31d7cfb356900 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -40,11 +40,11 @@ class HeaderUtility { */ class GetAllOfHeaderAsStringResult { public: - // The ultimate result of the concatenation. If absl::nullopt, no header values were found. + // The ultimate result of the concatenation. If std::nullopt, no header values were found. // If the final string required a string allocation, the memory is held in // backingString(). This allows zero allocation in the common case of a single header // value. - absl::optional result() const { + std::optional result() const { // This is safe for move/copy of this class as the backing string will be moved or copied. // Otherwise result_ is valid. The assert verifies that both are empty or only 1 is set. ASSERT((!result_.has_value() && result_backing_string_.empty()) || @@ -55,7 +55,7 @@ class HeaderUtility { const std::string& backingString() const { return result_backing_string_; } private: - absl::optional result_; + std::optional result_; // Valid only if result_ relies on memory allocation that must live beyond the call. See above. std::string result_backing_string_; @@ -335,6 +335,18 @@ class HeaderUtility { static bool matchHeaders(const HeaderMap& request_headers, const std::vector& config_headers); + /** + * See if any of the headers specified in the config are present in a request. + * @param request_headers supplies the headers from the request. + * @param config_headers supplies the list of configured header conditions on which to match. + * @return bool true if any of the headers (and values) in the config_headers are found in the + * request_headers. If no config_headers are specified, returns false. + */ + static bool matchAnyHeader(const HeaderMap& request_headers, + const std::vector& config_headers); + static bool matchAnyHeader(const HeaderMap& request_headers, + const std::vector& config_headers); + /** * Validates that a header value is valid, according to RFC 7230, section 3.2. * http://tools.ietf.org/html/rfc7230#section-3.2 @@ -414,9 +426,9 @@ class HeaderUtility { /** * Determines if request headers pass Envoy validity checks. * @param headers to validate - * @return details of the error if an error is present, otherwise absl::nullopt + * @return details of the error if an error is present, otherwise std::nullopt */ - static absl::optional> + static std::optional> requestHeadersValid(const RequestHeaderMap& headers); /** @@ -441,11 +453,11 @@ class HeaderUtility { /** * @brief Remove the port part from host/authority header if it is equal to provided port. - * @return absl::optional containing the port, if removed, else absl::nullopt. + * @return std::optional containing the port, if removed, else std::nullopt. * If port is not passed, port part from host/authority header is removed. */ - static absl::optional stripPortFromHost(RequestHeaderMap& headers, - absl::optional listener_port); + static std::optional stripPortFromHost(RequestHeaderMap& headers, + std::optional listener_port); /** * @brief Remove the port part from host if it exists. diff --git a/source/common/http/headers.h b/source/common/http/headers.h index ab31e2ed3a539..85670e99eb915 100644 --- a/source/common/http/headers.h +++ b/source/common/http/headers.h @@ -18,7 +18,7 @@ namespace Http { class PrefixValue { public: const char* prefix() { - absl::WriterMutexLock lock(&m_); + absl::WriterMutexLock lock(m_); read_ = true; return prefix_.c_str(); } @@ -26,7 +26,7 @@ class PrefixValue { // The char* prefix is used directly, so must be available for the interval where prefix() may be // called. void setPrefix(const char* prefix) { - absl::WriterMutexLock lock(&m_); + absl::WriterMutexLock lock(m_); // The check for unchanged string is purely for integration tests - this // should not happen in production. RELEASE_ASSERT(!read_ || prefix_ == std::string(prefix), @@ -338,6 +338,8 @@ class HeaderValues { struct { // per https://tools.ietf.org/html/draft-kinnear-httpbis-http2-transport-02 const std::string Bytestream{"bytestream"}; + // per https://datatracker.ietf.org/doc/draft-ietf-webtrans-overview/ + const std::string WebTransport{"webtransport"}; } ProtocolValues; struct { diff --git a/source/common/http/http1/BUILD b/source/common/http/http1/BUILD index f97da1de70a72..9b0672240f1c9 100644 --- a/source/common/http/http1/BUILD +++ b/source/common/http/http1/BUILD @@ -83,7 +83,6 @@ envoy_cc_library( "//source/common/http:headers_lib", "//source/common/runtime:runtime_features_lib", "//source/common/upstream:upstream_lib", - "@abseil-cpp//absl/types:optional", ], ) @@ -97,7 +96,6 @@ envoy_cc_library( "//source/common/common:matchers_lib", "//source/common/config:utility_lib", "//source/common/runtime:runtime_features_lib", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) diff --git a/source/common/http/http1/balsa_parser.cc b/source/common/http/http1/balsa_parser.cc index 33fa63cf303da..52e7f71e87f64 100644 --- a/source/common/http/http1/balsa_parser.cc +++ b/source/common/http/http1/balsa_parser.cc @@ -1,7 +1,9 @@ #include "source/common/http/http1/balsa_parser.h" #include +#include #include +#include #include #include "source/common/common/assert.h" @@ -25,22 +27,42 @@ constexpr absl::string_view kColonSlashSlash = "://"; constexpr char kResponseFirstByte = 'H'; constexpr absl::string_view kHttpVersionPrefix = "HTTP/"; -// Allowed characters for field names according to Section 5.1 -// and for methods according to Section 9.1 of RFC 9110: +// RFC 9110 Sections 5.1 and 9.1 define field names and methods as tokens: // https://www.rfc-editor.org/rfc/rfc9110.html -constexpr absl::string_view kValidCharacters = +constexpr char kValidCharacters[] = "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~"; -constexpr absl::string_view::iterator kValidCharactersBegin = kValidCharacters.begin(); -constexpr absl::string_view::iterator kValidCharactersEnd = kValidCharacters.end(); + +consteval std::array makeValidCharacterMask() { + std::array mask{}; + for (size_t i = 0; i < sizeof(kValidCharacters) - 1; ++i) { + const uint8_t index = static_cast(kValidCharacters[i]); + mask[index / 64] |= 1ULL << (index % 64); + } + return mask; +} + +// This keeps the per-character hot path branch-light and avoids a binary search through the valid +// character list for every byte in every HTTP/1 header name. +constexpr std::array kValidCharacterMask = makeValidCharacterMask(); + +constexpr bool isValidTokenCharacter(char c) { + const uint8_t index = static_cast(c); + return (kValidCharacterMask[index / 64] & (1ULL << (index % 64))) != 0; +} + +static_assert(isValidTokenCharacter('a')); +static_assert(isValidTokenCharacter('Z')); +static_assert(isValidTokenCharacter('-')); +static_assert(!isValidTokenCharacter(':')); +static_assert(!isValidTokenCharacter(' ')); // TODO(#21245): Skip method validation altogether when UHV method validation is // enabled. bool isMethodValid(absl::string_view method, bool allow_custom_methods) { if (allow_custom_methods) { return !method.empty() && - std::all_of(method.begin(), method.end(), [](absl::string_view::value_type c) { - return std::binary_search(kValidCharactersBegin, kValidCharactersEnd, c); - }); + std::all_of(method.begin(), method.end(), + [](absl::string_view::value_type c) { return isValidTokenCharacter(c); }); } static constexpr absl::string_view kValidMethods[] = { @@ -131,9 +153,8 @@ bool isVersionValid(absl::string_view version_input) { } bool isHeaderNameValid(absl::string_view name) { - return std::all_of(name.begin(), name.end(), [](absl::string_view::value_type c) { - return std::binary_search(kValidCharactersBegin, kValidCharactersEnd, c); - }); + return std::all_of(name.begin(), name.end(), + [](absl::string_view::value_type c) { return isValidTokenCharacter(c); }); } } // anonymous namespace @@ -153,6 +174,10 @@ BalsaParser::BalsaParser(MessageType type, ParserCallbacks* connection, size_t m http_validation_policy.require_content_length_if_body_required = false; http_validation_policy.disallow_invalid_header_characters_in_response = true; http_validation_policy.disallow_lone_cr_in_chunk_extension = true; + + http_validation_policy.disallow_stray_data_after_chunk = + Runtime::runtimeFeatureEnabled("envoy.reloadable_features.strict_chunk_parsing"); + framer_.set_http_validation_policy(http_validation_policy); framer_.set_balsa_headers(&headers_); @@ -237,9 +262,9 @@ bool BalsaParser::isHttp11() const { } } -absl::optional BalsaParser::contentLength() const { +std::optional BalsaParser::contentLength() const { if (!headers_.content_length_valid()) { - return absl::nullopt; + return std::nullopt; } return headers_.content_length(); } diff --git a/source/common/http/http1/balsa_parser.h b/source/common/http/http1/balsa_parser.h index 34a90ed4bf19f..30107365a2bc6 100644 --- a/source/common/http/http1/balsa_parser.h +++ b/source/common/http/http1/balsa_parser.h @@ -30,7 +30,7 @@ class BalsaParser : public Parser, public quiche::BalsaVisitorInterface { ParserStatus getStatus() const override; Http::Code statusCode() const override; bool isHttp11() const override; - absl::optional contentLength() const override; + std::optional contentLength() const override; bool isChunked() const override; absl::string_view methodName() const override; absl::string_view errorMessage() const override; diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index fef23db545003..a5ea9fd8e4263 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -104,9 +104,7 @@ static constexpr absl::string_view COLON_SPACE = ": "; StreamEncoderImpl::StreamEncoderImpl(ConnectionImpl& connection, StreamInfo::BytesMeterSharedPtr&& bytes_meter) - : connection_(connection), disable_chunk_encoding_(false), chunk_encoding_(true), - connect_request_(false), is_tcp_tunneling_(false), is_response_to_head_request_(false), - is_response_to_connect_request_(false), bytes_meter_(std::move(bytes_meter)) { + : connection_(connection), bytes_meter_(std::move(bytes_meter)) { if (!bytes_meter_) { bytes_meter_ = std::make_shared(); } @@ -143,7 +141,7 @@ void ResponseEncoderImpl::encode1xxHeaders(const ResponseHeaderMap& headers) { } void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& headers, - absl::optional status, bool end_stream, + std::optional status, bool end_stream, bool bodiless_request) { HeaderKeyFormatterOptConstRef formatter(headers.formatter()); if (!formatter.has_value()) { @@ -439,7 +437,7 @@ void ResponseEncoderImpl::encodeHeaders(const ResponseHeaderMap& headers, bool e is_response_to_connect_request_ = false; } - encodeHeadersBase(headers, absl::make_optional(numeric_status), end_stream, false); + encodeHeadersBase(headers, std::make_optional(numeric_status), end_stream, false); } static constexpr absl::string_view REQUEST_POSTFIX = " HTTP/1.1\r\n"; @@ -508,7 +506,7 @@ Status RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool e {method->value().getStringView(), SPACE, host_or_path_view, REQUEST_POSTFIX}); } - encodeHeadersBase(headers, absl::nullopt, end_stream, + encodeHeadersBase(headers, std::nullopt, end_stream, HeaderUtility::requestShouldHaveNoBody(headers)); return okStatus(); } @@ -535,9 +533,7 @@ ConnectionImpl::ConnectionImpl(Network::Connection& connection, CodecStats& stat uint32_t max_headers_kb, const uint32_t max_headers_count) : connection_(connection), stats_(stats), codec_settings_(settings), encode_only_header_key_formatter_(encodeOnlyFormatterFromSettings(settings)), - processing_trailers_(false), handling_upgrade_(false), reset_stream_called_(false), - deferred_end_stream_headers_(false), dispatching_(false), max_headers_kb_(max_headers_kb), - max_headers_count_(max_headers_count) { + max_headers_kb_(max_headers_kb), max_headers_count_(max_headers_count) { parser_ = std::make_unique(type, this, max_headers_kb_ * 1024, enableTrailers(), codec_settings_.allow_custom_methods_); } @@ -1061,7 +1057,7 @@ void ClientConnectionImpl::dumpAdditionalState(std::ostream& os, int indent_leve os << spaces << "Dumping corresponding downstream request:"; if (pending_response_.has_value()) { os << '\n'; - const ResponseDecoder* decoder = pending_response_.value().decoder_; + const ResponseDecoder* decoder = pending_response_.value().decoder_handle_->get().ptr(); DUMP_DETAILS(decoder); } else { os << " null\n"; @@ -1250,7 +1246,11 @@ Envoy::StatusOr ServerConnectionImpl::onHeadersCompleteBase() { if (parser_->isChunked() || (parser_->contentLength().has_value() && parser_->contentLength().value() > 0) || handling_upgrade_) { - active_request_->request_decoder_->decodeHeaders(std::move(headers), false); + RequestDecoder* decoder = active_request_->request_decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "RequestDecoder is null in onHeadersCompleteBase"); + if (decoder) { + decoder->decodeHeaders(std::move(headers), false); + } // If the connection has been closed (or is closing) after decoding headers, pause the parser // so we return control to the caller. @@ -1269,7 +1269,8 @@ Status ServerConnectionImpl::onMessageBeginBase() { if (!resetStreamCalled()) { ASSERT(active_request_ == nullptr); active_request_ = std::make_unique(*this, std::move(bytes_meter_before_stream_)); - active_request_->request_decoder_ = &callbacks_.newStream(active_request_->response_encoder_); + active_request_->request_decoder_handle_ = + callbacks_.newStream(active_request_->response_encoder_).getRequestDecoderHandle(); // Check for pipelined request flood as we prepare to accept a new request. // Parse errors that happen prior to onMessageBegin result in stream termination, it is not @@ -1293,7 +1294,11 @@ void ServerConnectionImpl::onBody(Buffer::Instance& data) { ASSERT(!deferred_end_stream_headers_); if (active_request_) { ENVOY_CONN_LOG(trace, "body size={}", connection_, data.length()); - active_request_->request_decoder_->decodeData(data, false); + RequestDecoder* decoder = active_request_->request_decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "RequestDecoder is null in onBody"); + if (decoder) { + decoder->decodeData(data, false); + } } } @@ -1329,19 +1334,25 @@ CallbackResult ServerConnectionImpl::onMessageCompleteBase() { if (active_request_) { // The request_decoder should be non-null after we've called the newStream on callbacks. - ASSERT(active_request_->request_decoder_); + RequestDecoder* decoder = active_request_->request_decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "request_decoder_handle_ is null in onMessageCompleteBase"); active_request_->remote_complete_ = true; if (deferred_end_stream_headers_) { - active_request_->request_decoder_->decodeHeaders( - std::move(absl::get(headers_or_trailers_)), true); + if (decoder) { + decoder->decodeHeaders(std::move(absl::get(headers_or_trailers_)), + true); + } deferred_end_stream_headers_ = false; } else if (processing_trailers_) { - active_request_->request_decoder_->decodeTrailers( - std::move(absl::get(headers_or_trailers_))); + if (decoder) { + decoder->decodeTrailers(std::move(absl::get(headers_or_trailers_))); + } } else { Buffer::OwnedImpl buffer; - active_request_->request_decoder_->decodeData(buffer, true); + if (decoder) { + decoder->decodeData(buffer, true); + } } // Reset to ensure no information from one requests persists to the next. @@ -1386,8 +1397,12 @@ Status ServerConnectionImpl::sendProtocolError(absl::string_view details) { active_request_->response_encoder_.setDetails(details); if (!active_request_->response_encoder_.startedResponse()) { - active_request_->request_decoder_->sendLocalReply( - error_code_, CodeUtility::toString(error_code_), nullptr, absl::nullopt, details); + RequestDecoder* decoder = active_request_->request_decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "RequestDecoder is null in sendProtocolError"); + if (decoder) { + decoder->sendLocalReply(error_code_, CodeUtility::toString(error_code_), nullptr, + std::nullopt, details); + } } return okStatus(); } @@ -1447,7 +1462,7 @@ void ServerConnectionImpl::ActiveRequest::dumpState(std::ostream& os, int indent ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, CodecStats& stats, ConnectionCallbacks&, const Http1Settings& settings, - absl::optional max_response_headers_kb, + std::optional max_response_headers_kb, const uint32_t max_response_headers_count, bool passing_through_proxy) : ConnectionImpl(connection, stats, settings, MessageType::Response, @@ -1487,7 +1502,8 @@ RequestEncoder& ClientConnectionImpl::newStream(ResponseDecoder& response_decode ASSERT(!pending_response_.has_value()); ASSERT(pending_response_done_); - pending_response_.emplace(*this, std::move(bytes_meter_before_stream_), &response_decoder); + pending_response_.emplace(*this, std::move(bytes_meter_before_stream_), + response_decoder.createResponseDecoderHandle()); pending_response_done_ = false; return pending_response_.value().encoder_; } @@ -1544,12 +1560,16 @@ Envoy::StatusOr ClientConnectionImpl::onHeadersCompleteBase() { } } - if (HeaderUtility::isSpecial1xx(*headers)) { - pending_response_.value().decoder_->decode1xxHeaders(std::move(headers)); - } else if (cannotHaveBody() && !handling_upgrade_) { - deferred_end_stream_headers_ = true; - } else { - pending_response_.value().decoder_->decodeHeaders(std::move(headers), false); + ResponseDecoder* decoder = pending_response_.value().decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "ResponseDecoder is null in onHeadersCompleteBase"); + if (decoder) { + if (HeaderUtility::isSpecial1xx(*headers)) { + decoder->decode1xxHeaders(std::move(headers)); + } else if (cannotHaveBody() && !handling_upgrade_) { + deferred_end_stream_headers_ = true; + } else { + decoder->decodeHeaders(std::move(headers), false); + } } // http-parser treats 1xx headers as their own complete response. Swallow the spurious @@ -1580,7 +1600,11 @@ void ClientConnectionImpl::onBody(Buffer::Instance& data) { ASSERT(!deferred_end_stream_headers_); if (pending_response_.has_value()) { ASSERT(!pending_response_done_); - pending_response_.value().decoder_->decodeData(data, false); + ResponseDecoder* decoder = pending_response_.value().decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "ResponseDecoder is null in onBody"); + if (decoder) { + decoder->decodeData(data, false); + } } } @@ -1598,16 +1622,19 @@ CallbackResult ClientConnectionImpl::onMessageCompleteBase() { // be reset just yet. Preserve the state in pending_response_done_ instead. pending_response_done_ = true; - if (deferred_end_stream_headers_) { - response.decoder_->decodeHeaders( - std::move(absl::get(headers_or_trailers_)), true); - deferred_end_stream_headers_ = false; - } else if (processing_trailers_) { - response.decoder_->decodeTrailers( - std::move(absl::get(headers_or_trailers_))); - } else { - Buffer::OwnedImpl buffer; - response.decoder_->decodeData(buffer, true); + ResponseDecoder* decoder = response.decoder_handle_->get().ptr(); + ENVOY_BUG(decoder != nullptr, "ResponseDecoder is null in onMessageCompleteBase"); + if (decoder) { + if (deferred_end_stream_headers_) { + decoder->decodeHeaders(std::move(absl::get(headers_or_trailers_)), + true); + deferred_end_stream_headers_ = false; + } else if (processing_trailers_) { + decoder->decodeTrailers(std::move(absl::get(headers_or_trailers_))); + } else { + Buffer::OwnedImpl buffer; + decoder->decodeData(buffer, true); + } } if (force_reset_on_premature_upstream_half_close_ && !encode_complete_) { diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index 8e0f5d83ca8d9..42f72fefce3f6 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -91,23 +91,23 @@ class StreamEncoderImpl : public virtual StreamEncoder, const StreamInfo::BytesMeterSharedPtr& bytesMeter() override { return bytes_meter_; } // http1 doesn't have a codec level stream id. - absl::optional codecStreamId() const override { return absl::nullopt; } + std::optional codecStreamId() const override { return std::nullopt; } protected: StreamEncoderImpl(ConnectionImpl& connection, StreamInfo::BytesMeterSharedPtr&& bytes_meter); - void encodeHeadersBase(const RequestOrResponseHeaderMap& headers, absl::optional status, + void encodeHeadersBase(const RequestOrResponseHeaderMap& headers, std::optional status, bool end_stream, bool bodiless_request); void encodeTrailersBase(const HeaderMap& headers); Buffer::BufferMemoryAccountSharedPtr buffer_memory_account_; ConnectionImpl& connection_; uint32_t read_disable_calls_{}; - bool disable_chunk_encoding_ : 1; - bool chunk_encoding_ : 1; - bool connect_request_ : 1; - bool is_tcp_tunneling_ : 1; - bool is_response_to_head_request_ : 1; - bool is_response_to_connect_request_ : 1; + bool disable_chunk_encoding_ : 1 = false; + bool chunk_encoding_ : 1 = true; + bool connect_request_ : 1 = false; + bool is_tcp_tunneling_ : 1 = false; + bool is_response_to_head_request_ : 1 = false; + bool is_response_to_connect_request_ : 1 = false; private: /** @@ -311,14 +311,14 @@ class ConnectionImpl : public virtual Connection, const HeaderKeyFormatterConstPtr encode_only_header_key_formatter_; HeaderString current_header_field_; HeaderString current_header_value_; - bool processing_trailers_ : 1; - bool handling_upgrade_ : 1; - bool reset_stream_called_ : 1; + bool processing_trailers_ : 1 = false; + bool handling_upgrade_ : 1 = false; + bool reset_stream_called_ : 1 = false; // Deferred end stream headers indicate that we are not going to raise headers until the full // HTTP/1 message has been flushed from the parser. This allows raising an HTTP/2 style headers // block with end stream set to true with no further protocol data remaining. - bool deferred_end_stream_headers_ : 1; - bool dispatching_ : 1; + bool deferred_end_stream_headers_ : 1 = false; + bool dispatching_ : 1 = false; bool dispatching_slice_already_drained_ : 1; StreamInfo::BytesMeterSharedPtr bytes_meter_before_stream_; const uint32_t max_headers_kb_; @@ -475,7 +475,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { void dumpState(std::ostream& os, int indent_level) const; HeaderString request_url_; - RequestDecoder* request_decoder_{}; + RequestDecoderHandlePtr request_decoder_handle_; ResponseEncoderImpl response_encoder_; bool remote_complete_{}; }; @@ -579,7 +579,7 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { public: ClientConnectionImpl(Network::Connection& connection, CodecStats& stats, ConnectionCallbacks& callbacks, const Http1Settings& settings, - absl::optional max_response_headers_kb, + std::optional max_response_headers_kb, const uint32_t max_response_headers_count, bool passing_through_proxy = false); // Http::ClientConnection @@ -588,10 +588,11 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { private: struct PendingResponse { PendingResponse(ConnectionImpl& connection, StreamInfo::BytesMeterSharedPtr&& bytes_meter, - ResponseDecoder* decoder) - : encoder_(connection, std::move(bytes_meter)), decoder_(decoder) {} + ResponseDecoderHandlePtr&& decoder_handle) + : encoder_(connection, std::move(bytes_meter)), decoder_handle_(std::move(decoder_handle)) { + } RequestEncoderImpl encoder_; - ResponseDecoder* decoder_; + ResponseDecoderHandlePtr decoder_handle_; }; bool cannotHaveBody(); @@ -657,7 +658,7 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { // buffer. This buffer is always allocated, never nullptr. Buffer::InstancePtr owned_output_buffer_; - absl::optional pending_response_; + std::optional pending_response_; // TODO(mattklein123): The following bool tracks whether a pending response is complete before // dispatching callbacks. This is needed so that pending_response_ stays valid during callbacks // in order to access the stream, but to avoid invoking callbacks that shouldn't be called once diff --git a/source/common/http/http1/conn_pool.cc b/source/common/http/http1/conn_pool.cc index a5261109c8be2..9cff6bf507ad2 100644 --- a/source/common/http/http1/conn_pool.cc +++ b/source/common/http/http1/conn_pool.cc @@ -116,7 +116,7 @@ allocateConnPool(Event::Dispatcher& dispatcher, Random::RandomGenerator& random_ std::move(host), std::move(priority), dispatcher, options, transport_socket_options, random_generator, state, [](HttpConnPoolImplBase* pool) { - return std::make_unique(*pool, absl::nullopt); + return std::make_unique(*pool, std::nullopt); }, [](Upstream::Host::CreateConnectionData& data, HttpConnPoolImplBase* pool) { CodecClientPtr codec{new CodecClientProd( @@ -124,7 +124,7 @@ allocateConnPool(Event::Dispatcher& dispatcher, Random::RandomGenerator& random_ pool->dispatcher(), pool->randomGenerator(), pool->transportSocketOptions())}; return codec; }, - std::vector{Protocol::Http11}, overload_manager, absl::nullopt, nullptr); + std::vector{Protocol::Http11}, overload_manager, std::nullopt, nullptr); } } // namespace Http1 diff --git a/source/common/http/http1/legacy_parser_impl.cc b/source/common/http/http1/legacy_parser_impl.cc index b41f4befd1430..452aca37c6839 100644 --- a/source/common/http/http1/legacy_parser_impl.cc +++ b/source/common/http/http1/legacy_parser_impl.cc @@ -97,12 +97,12 @@ class LegacyHttpParserImpl::Impl { bool isHttp11() const { return parser_.http_major == 1 && parser_.http_minor == 1; } - absl::optional contentLength() const { + std::optional contentLength() const { // An unset content length will be have all bits set. // See // https://github.com/nodejs/http-parser/blob/ec8b5ee63f0e51191ea43bb0c6eac7bfbff3141d/http_parser.h#L311 if (parser_.content_length == ULLONG_MAX) { - return absl::nullopt; + return std::nullopt; } return parser_.content_length; } @@ -152,7 +152,7 @@ Http::Code LegacyHttpParserImpl::statusCode() const { return impl_->statusCode() bool LegacyHttpParserImpl::isHttp11() const { return impl_->isHttp11(); } -absl::optional LegacyHttpParserImpl::contentLength() const { +std::optional LegacyHttpParserImpl::contentLength() const { return impl_->contentLength(); } diff --git a/source/common/http/http1/legacy_parser_impl.h b/source/common/http/http1/legacy_parser_impl.h index a7a5ae31b31d1..806b09e91e32c 100644 --- a/source/common/http/http1/legacy_parser_impl.h +++ b/source/common/http/http1/legacy_parser_impl.h @@ -20,7 +20,7 @@ class LegacyHttpParserImpl : public Parser { ParserStatus getStatus() const override; Http::Code statusCode() const override; bool isHttp11() const override; - absl::optional contentLength() const override; + std::optional contentLength() const override; bool isChunked() const override; absl::string_view methodName() const override; absl::string_view errorMessage() const override; diff --git a/source/common/http/http1/parser.h b/source/common/http/http1/parser.h index fb781af1b5731..3b73dd1626a05 100644 --- a/source/common/http/http1/parser.h +++ b/source/common/http/http1/parser.h @@ -136,8 +136,8 @@ class Parser { // Returns whether HTTP version is 1.1. virtual bool isHttp11() const PURE; - // Returns the number of bytes in the body. absl::nullopt if no Content-Length header - virtual absl::optional contentLength() const PURE; + // Returns the number of bytes in the body. std::nullopt if no Content-Length header + virtual std::optional contentLength() const PURE; // Returns whether headers are chunked. virtual bool isChunked() const PURE; diff --git a/source/common/http/http2/BUILD b/source/common/http/http2/BUILD index da5201ffc2b23..b9e940752f005 100644 --- a/source/common/http/http2/BUILD +++ b/source/common/http/http2/BUILD @@ -39,6 +39,7 @@ envoy_cc_library( "//envoy/http:codes_interface", "//envoy/http:header_map_interface", "//envoy/network:connection_interface", + "//envoy/runtime:runtime_interface", "//envoy/server/overload:overload_manager_interface", "//envoy/stats:stats_interface", "//source/common/buffer:buffer_lib", @@ -60,7 +61,6 @@ envoy_cc_library( "@abseil-cpp//absl/algorithm", "@abseil-cpp//absl/cleanup", "@abseil-cpp//absl/container:inlined_vector", - "@abseil-cpp//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@quiche//:http2_adapter", ] + envoy_select_nghttp2([envoy_external_dep_path("nghttp2")]), diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index 3752eaf92f4d0..7af0cbad976f9 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -10,6 +10,7 @@ #include "envoy/http/codes.h" #include "envoy/http/header_map.h" #include "envoy/network/connection.h" +#include "envoy/runtime/runtime.h" #include "source/common/common/assert.h" #include "source/common/common/cleanup.h" @@ -137,6 +138,9 @@ class Http2ResponseCodeDetailValues { const absl::string_view oghttp2_err_unknown_ = "http2.unknown.oghttp2.error"; // The number of headers (or trailers) exceeded the configured limits const absl::string_view too_many_headers = "http2.too_many_headers"; + // The total size of headers exceeded the configured limits + const absl::string_view header_list_size_too_large = "http2.header_list_size_too_large"; + const absl::string_view cookies_total_bytes_too_large = "http2.cookies_total_bytes_too_large"; // Envoy detected an HTTP/2 frame flood from the server. const absl::string_view outbound_frame_flood = "http2.outbound_frames_flood"; // Envoy detected an inbound HTTP/2 frame flood. @@ -179,6 +183,8 @@ int reasonToReset(StreamResetReason reason, bool response_end_stream_sent) { return OGHTTP2_REFUSED_STREAM; case StreamResetReason::ConnectError: return OGHTTP2_CONNECT_ERROR; + case StreamResetReason::RemoteResetNoError: + return OGHTTP2_NO_ERROR; case StreamResetReason::ProtocolError: if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.reset_with_error")) { return OGHTTP2_NO_ERROR; @@ -213,7 +219,7 @@ using OnHeaderResult = http2::adapter::Http2VisitorInterface::OnHeaderResult; enum Settings { // SETTINGS_HEADER_TABLE_SIZE = 0x01, // SETTINGS_ENABLE_PUSH = 0x02, - SETTINGS_MAX_CONCURRENT_STREAMS = 0x03, + SETTINGS_MAX_CONCURRENT_STREAMS = 0x03, // NOLINT(readability-identifier-naming) // SETTINGS_INITIAL_WINDOW_SIZE = 0x04, // SETTINGS_MAX_FRAME_SIZE = 0x05, // SETTINGS_MAX_HEADER_LIST_SIZE = 0x06, @@ -223,9 +229,9 @@ enum Settings { enum Flags { // FLAG_NONE = 0, - FLAG_END_STREAM = 0x01, + FLAG_END_STREAM = 0x01, // NOLINT(readability-identifier-naming) // FLAG_END_HEADERS = 0x04, - FLAG_ACK = 0x01, + FLAG_ACK = 0x01, // NOLINT(readability-identifier-naming) // FLAG_PADDED = 0x08, // FLAG_PRIORITY = 0x20 }; @@ -302,11 +308,11 @@ ConnectionImpl::StreamImpl::StreamImpl(ConnectionImpl& parent, uint32_t buffer_l [this]() -> void { this->pendingSendBufferLowWatermark(); }, [this]() -> void { this->pendingSendBufferHighWatermark(); }, []() -> void { /* TODO(adisuissa): Handle overflow watermark */ })), - local_end_stream_sent_(false), remote_end_stream_(false), remote_rst_(false), - data_deferred_(false), received_noninformational_headers_(false), + cookie_count_(0), local_end_stream_sent_(false), remote_end_stream_(false), + remote_rst_(false), data_deferred_(false), received_noninformational_headers_(false), pending_receive_buffer_high_watermark_called_(false), pending_send_buffer_high_watermark_called_(false), reset_due_to_messaging_error_(false), - extend_stream_lifetime_flag_(false) { + extend_stream_lifetime_flag_(false), histograms_recorded_(false) { parent_.stats_.streams_active_.inc(); if (buffer_limit > 0) { setWriteBufferWatermarks(buffer_limit); @@ -650,7 +656,7 @@ void ConnectionImpl::ClientStreamImpl::decodeHeaders() { // In UHV mode the :status header at this point can be malformed, as it is validated // later on in the response_decoder_.decodeHeaders() call. // Account for this here. - absl::optional status_opt = Http::Utility::getResponseStatusOrNullopt(*headers); + std::optional status_opt = Http::Utility::getResponseStatusOrNullopt(*headers); if (!status_opt.has_value()) { // In case the status is invalid or missing, the response_decoder_.decodeHeaders() will fail the // request @@ -747,7 +753,9 @@ void ConnectionImpl::StreamImpl::pendingSendBufferLowWatermark() { } void ConnectionImpl::StreamImpl::saveHeader(HeaderString&& name, HeaderString&& value) { - if (!Utility::reconstituteCrumbledCookies(name, value, cookies_)) { + if (Utility::reconstituteCrumbledCookies(name, value, cookies_)) { + cookie_count_++; + } else { headers().addViaMove(std::move(name), std::move(value)); } } @@ -977,14 +985,24 @@ void ConnectionImpl::StreamImpl::setAccount(Buffer::BufferMemoryAccountSharedPtr ConnectionImpl::ConnectionImpl(Network::Connection& connection, CodecStats& stats, Random::RandomGenerator& random_generator, const envoy::config::core::v3::Http2ProtocolOptions& http2_options, - const uint32_t max_headers_kb, const uint32_t max_headers_count) + const uint32_t max_headers_kb, const uint32_t max_headers_count, + OptRef runtime) : stats_(stats), connection_(connection), max_headers_kb_(max_headers_kb), max_headers_count_(max_headers_count), per_stream_buffer_limit_(http2_options.initial_stream_window_size().value()), stream_error_on_invalid_http_messaging_( http2_options.override_stream_error_on_invalid_http_message().value()), - protocol_constraints_(stats, http2_options), dispatching_(false), raised_goaway_(false), - random_(random_generator), + record_http2_histograms_( + Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_record_histograms")), + max_cookie_size_bytes_( + runtime.has_value() ? runtime->snapshot().getInteger( + "envoy.reloadable_features.http2_max_cookies_size_in_kb", 0) * + 1024 + : 0), + protocol_constraints_(stats, http2_options, + Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.http2_flood_protection_active_streams")), + dispatching_(false), raised_goaway_(false), random_(random_generator), last_received_data_time_(connection_.dispatcher().timeSource().monotonicTime()) { if (http2_options.has_use_oghttp2_codec()) { use_oghttp2_library_ = http2_options.use_oghttp2_codec().value(); @@ -1274,6 +1292,7 @@ Status ConnectionImpl::onHeaders(int32_t stream_id, size_t length, uint8_t flags stream->bytes_meter_->addHeaderBytesReceived(length + H2_FRAME_HEADER_SIZE); stream->remote_end_stream_ = flags & FLAG_END_STREAM; + recordHistogramsForStream(*stream); if (!stream->cookies_.empty()) { HeaderString key(Headers::get().Cookie); stream->headers().addViaMove(std::move(key), std::move(stream->cookies_)); @@ -1513,6 +1532,8 @@ Status ConnectionImpl::onStreamClose(StreamImpl* stream, uint32_t error_code) { if (stream) { const int32_t stream_id = stream->stream_id_; + recordHistogramsForStream(*stream); + // Consume buffered on stream_close. if (stream->stream_manager_.buffered_on_stream_close_) { stream->stream_manager_.buffered_on_stream_close_ = false; @@ -1533,42 +1554,56 @@ Status ConnectionImpl::onStreamClose(StreamImpl* stream, uint32_t error_code) { } if (should_reset_stream) { - StreamResetReason reason; - if (stream->reset_due_to_messaging_error_) { - // Unfortunately, the nghttp2 API makes it incredibly difficult to clearly understand - // the flow of resets. I.e., did the reset originate locally? Was it remote? Here, - // we attempt to track cases in which we sent a reset locally due to an invalid frame - // received from the remote. We only do that in two cases currently (HTTP messaging layer - // errors from https://tools.ietf.org/html/rfc7540#section-8 which nghttp2 is very strict - // about). In other cases we treat invalid frames as a protocol error and just kill - // the connection. - - // Get ClientConnectionImpl or ServerConnectionImpl specific stream reset reason, - // depending whether the connection is upstream or downstream. - reason = getMessagingErrorResetReason(); + // RFC 9113 Section 8.1: A server MAY send RST_STREAM(NO_ERROR) after sending + // a complete response. The complete response MUST NOT be discarded. + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http_preserve_rst_no_error") && + stream->remote_end_stream_ && error_code == OGHTTP2_NO_ERROR && + !stream->reset_reason_.has_value()) { + if (stream->stream_manager_.hasBufferedBodyOrTrailers()) { + ENVOY_CONN_LOG(debug, "buffered onStreamClose for stream: {}", connection_, stream_id); + stream->stream_manager_.buffered_on_stream_close_ = true; + stats_.deferred_stream_close_.inc(); + return okStatus(); + } + stream->runResetCallbacks(StreamResetReason::RemoteResetNoError, absl::string_view()); } else { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.reset_with_error")) { - reason = errorCodeToResetReason(error_code); - if (error_code == OGHTTP2_REFUSED_STREAM) { - stream->setDetails(Http2ResponseCodeDetails::get().remote_refused); - } else { - stream->setDetails(Http2ResponseCodeDetails::get().remote_reset); - } + StreamResetReason reason; + if (stream->reset_due_to_messaging_error_) { + // Unfortunately, the nghttp2 API makes it incredibly difficult to clearly understand + // the flow of resets. I.e., did the reset originate locally? Was it remote? Here, + // we attempt to track cases in which we sent a reset locally due to an invalid frame + // received from the remote. We only do that in two cases currently (HTTP messaging layer + // errors from https://tools.ietf.org/html/rfc7540#section-8 which nghttp2 is very strict + // about). In other cases we treat invalid frames as a protocol error and just kill + // the connection. + + // Get ClientConnectionImpl or ServerConnectionImpl specific stream reset reason, + // depending whether the connection is upstream or downstream. + reason = getMessagingErrorResetReason(); } else { - if (error_code == OGHTTP2_REFUSED_STREAM) { - reason = StreamResetReason::RemoteRefusedStreamReset; - stream->setDetails(Http2ResponseCodeDetails::get().remote_refused); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.reset_with_error")) { + reason = errorCodeToResetReason(error_code); + if (error_code == OGHTTP2_REFUSED_STREAM) { + stream->setDetails(Http2ResponseCodeDetails::get().remote_refused); + } else { + stream->setDetails(Http2ResponseCodeDetails::get().remote_reset); + } } else { - if (error_code == OGHTTP2_CONNECT_ERROR) { - reason = StreamResetReason::ConnectError; + if (error_code == OGHTTP2_REFUSED_STREAM) { + reason = StreamResetReason::RemoteRefusedStreamReset; + stream->setDetails(Http2ResponseCodeDetails::get().remote_refused); } else { - reason = StreamResetReason::RemoteReset; + if (error_code == OGHTTP2_CONNECT_ERROR) { + reason = StreamResetReason::ConnectError; + } else { + reason = StreamResetReason::RemoteReset; + } + stream->setDetails(Http2ResponseCodeDetails::get().remote_reset); } - stream->setDetails(Http2ResponseCodeDetails::get().remote_reset); } } + stream->runResetCallbacks(reason, absl::string_view()); } - stream->runResetCallbacks(reason, absl::string_view()); } else if (!stream->reset_reason_.has_value() && stream->stream_manager_.hasBufferedBodyOrTrailers()) { @@ -1580,6 +1615,7 @@ Status ConnectionImpl::onStreamClose(StreamImpl* stream, uint32_t error_code) { return okStatus(); } + protocol_constraints_.decrementActiveStreamCount(); stream->destroy(); current_stream_id_.reset(); // TODO(antoniovicente) Test coverage for onCloseStream before deferred reset handling happens. @@ -1636,6 +1672,26 @@ int ConnectionImpl::onMetadataFrameComplete(int32_t stream_id, bool end_metadata return result ? 0 : ERR_CALLBACK_FAILURE; } +// This function can be invoked multiple times for a single stream: +// - Once in onHeaders() for request/response headers (this is when recording happens). +// - Again in onHeaders() if there are trailers (ignored, trailers are not recorded). +// - Once in onStreamClose() as a fallback if headers weren't recorded (e.g. early error), +// or as a redundant call for successful streams (ignored). +// The `histograms_recorded_` guard ensures we only record once (only for headers, not trailers). +void ConnectionImpl::recordHistogramsForStream(StreamImpl& stream) { + if (record_http2_histograms_ && !stream.histograms_recorded_) { + uint64_t headers_size = stream.headers().byteSize(); + uint64_t headers_count = stream.headers().size(); + uint64_t headers_with_cookies_size = headers_size + stream.cookies_.size(); + uint64_t headers_with_cookies_count = headers_count + stream.cookie_count_; + stats_.header_list_size_.recordValue(headers_with_cookies_size); + stats_.cookie_size_.recordValue(stream.cookies_.size()); + stats_.header_count_.recordValue(headers_with_cookies_count); + stats_.cookie_count_.recordValue(stream.cookie_count_); + stream.histograms_recorded_ = true; + } +} + int ConnectionImpl::saveHeader(int32_t stream_id, HeaderString&& name, HeaderString&& value) { StreamImpl* stream = getStreamUnchecked(stream_id); if (!stream) { @@ -1661,16 +1717,33 @@ int ConnectionImpl::saveHeader(int32_t stream_id, HeaderString&& name, HeaderStr } stream->saveHeader(std::move(name), std::move(value)); + const uint64_t total_cookie_size = stream->cookies_.size(); + if (max_cookie_size_bytes_ > 0 && total_cookie_size > max_cookie_size_bytes_) { + stream->setDetails(Http2ResponseCodeDetails::get().cookies_total_bytes_too_large); + stats_.cookies_total_bytes_too_large_.inc(); + return ERR_TEMPORAL_CALLBACK_FAILURE; + } + uint64_t headers_size = stream->headers().byteSize(); + uint64_t headers_count = stream->headers().size(); - if (stream->headers().byteSize() > max_headers_kb_ * 1024 || - stream->headers().size() > max_headers_count_) { + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_include_cookies_in_limits")) { + headers_size += stream->cookies_.size(); + headers_count += stream->cookie_count_; + } + + if (headers_size > max_headers_kb_ * 1024) { + stream->setDetails(Http2ResponseCodeDetails::get().header_list_size_too_large); + stats_.header_list_size_too_large_.inc(); + return ERR_TEMPORAL_CALLBACK_FAILURE; + } + if (headers_count > max_headers_count_) { stream->setDetails(Http2ResponseCodeDetails::get().too_many_headers); stats_.header_overflow_.inc(); // This will cause the library to reset/close the stream. return ERR_TEMPORAL_CALLBACK_FAILURE; - } else { - return 0; } + + return 0; } Status ConnectionImpl::sendPendingFrames() { @@ -1818,6 +1891,7 @@ void ConnectionImpl::onProtocolConstraintViolation() { void ConnectionImpl::onUnderlyingConnectionBelowWriteBufferLowWatermark() { // Notify the streams based on least recently encoding to the connection. + // NOLINTNEXTLINE(modernize-loop-convert) for (auto it = active_streams_.rbegin(); it != active_streams_.rend(); ++it) { (*it)->runLowWatermarkCallbacks(); } @@ -2082,6 +2156,11 @@ ConnectionImpl::Http2Options::Http2Options( og_options_.max_header_field_size = max_headers_kb * 1024; og_options_.allow_extended_connect = http2_options.allow_connect(); og_options_.allow_different_host_and_authority = true; + og_options_.allow_obs_text = + !PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, disallow_obs_text, false); + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_include_cookies_in_limits")) { + og_options_.enforce_max_header_list_bytes = true; + } if (!PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, enable_huffman_encoding, true)) { if (http2_options.has_hpack_table_size() && http2_options.hpack_table_size().value() == 0) { og_options_.compression_option = http2::adapter::OgHttp2Session::Options::DISABLE_COMPRESSION; @@ -2146,6 +2225,16 @@ ConnectionImpl::Http2Options::Http2Options( // 512 is chosen to accommodate Envoy's 8Mb max limit of max_request_headers_kb // in both headers and trailers nghttp2_option_set_max_continuations(options_, 512); + + // Configure the RST_STREAM rate limiter (CVE-2023-44487 / HTTP/2 Rapid Reset protection). + // nghttp2 defaults: burst=1000, rate=33/sec. Allow operators to tune these when legitimate + // workloads (e.g. gRPC with many short-lived streams that get RST_STREAM on RESOURCE_EXHAUSTED) + // exhaust the default token budget faster than it replenishes. + if (http2_options.has_stream_reset_burst() || http2_options.has_stream_reset_rate()) { + const uint64_t burst = PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, stream_reset_burst, 1000); + const uint64_t rate = PROTOBUF_GET_WRAPPED_OR_DEFAULT(http2_options, stream_reset_rate, 33); + nghttp2_option_set_stream_reset_rate_limit(options_, burst, rate); + } #endif } @@ -2370,9 +2459,9 @@ ServerConnectionImpl::ServerConnectionImpl( const uint32_t max_request_headers_kb, const uint32_t max_request_headers_count, envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action, - Server::OverloadManager& overload_manager) + Server::OverloadManager& overload_manager, OptRef runtime) : ConnectionImpl(connection, stats, random_generator, http2_options, max_request_headers_kb, - max_request_headers_count), + max_request_headers_count, runtime), callbacks_(callbacks), headers_with_underscores_action_(headers_with_underscores_action), should_send_go_away_on_dispatch_(overload_manager.getLoadShedPoint( Server::LoadShedPointName::get().H2ServerGoAwayOnDispatch)), @@ -2450,22 +2539,25 @@ int ServerConnectionImpl::onHeader(int32_t stream_id, HeaderString&& name, Heade Http::Status ServerConnectionImpl::dispatch(Buffer::Instance& data) { // Make sure downstream outbound queue was not flooded by the upstream frames. RETURN_IF_ERROR(protocol_constraints_.checkOutboundFrameLimits()); - if (should_send_go_away_and_close_on_dispatch_ != nullptr && - should_send_go_away_and_close_on_dispatch_->shouldShedLoad()) { - ConnectionImpl::goAway(); - sent_go_away_on_dispatch_ = true; - return envoyOverloadError( - "Load shed point http2_server_go_away_and_close_on_dispatch triggered"); - } - if (should_send_go_away_on_dispatch_ != nullptr && !sent_go_away_on_dispatch_ && - should_send_go_away_on_dispatch_->shouldShedLoad()) { - ConnectionImpl::goAway(); - sent_go_away_on_dispatch_ = true; + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.http2_fix_goaway_loadshed_point")) { + if (should_send_go_away_and_close_on_dispatch_ != nullptr && + should_send_go_away_and_close_on_dispatch_->shouldShedLoad()) { + ConnectionImpl::goAway(); + sent_go_away_on_dispatch_ = true; + return envoyOverloadError( + "Load shed point http2_server_go_away_and_close_on_dispatch triggered"); + } + if (should_send_go_away_on_dispatch_ != nullptr && !sent_go_away_on_dispatch_ && + should_send_go_away_on_dispatch_->shouldShedLoad()) { + ConnectionImpl::goAway(); + sent_go_away_on_dispatch_ = true; + } } return ConnectionImpl::dispatch(data); } -absl::optional ServerConnectionImpl::checkHeaderNameForUnderscores( +std::optional ServerConnectionImpl::checkHeaderNameForUnderscores( [[maybe_unused]] absl::string_view header_name) { #ifndef ENVOY_ENABLE_UHV // This check has been moved to UHV @@ -2487,7 +2579,7 @@ absl::optional ServerConnectionImpl::checkHeaderNameForUnderscores( // Workaround for gcc not understanding [[maybe_unused]] for class members. (void)headers_with_underscores_action_; #endif - return absl::nullopt; + return std::nullopt; } } // namespace Http2 diff --git a/source/common/http/http2/codec_impl.h b/source/common/http/http2/codec_impl.h index 20a620efdc2d9..2ba83f24912ec 100644 --- a/source/common/http/http2/codec_impl.h +++ b/source/common/http/http2/codec_impl.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include "envoy/event/deferred_deletable.h" #include "envoy/http/codec.h" #include "envoy/network/connection.h" +#include "envoy/runtime/runtime.h" #include "envoy/server/overload/overload_manager.h" #include "source/common/buffer/buffer_impl.h" @@ -29,7 +31,6 @@ #include "source/common/http/http2/protocol_constraints.h" #include "source/common/http/status.h" -#include "absl/types/optional.h" #include "absl/types/span.h" #ifdef ENVOY_NGHTTP2 @@ -44,6 +45,7 @@ namespace Http { namespace Http2 { // Types inherited from nghttp2 and preserved in oghttp2 +// NOLINTBEGIN(readability-identifier-naming) enum ErrorType { OGHTTP2_NO_ERROR, OGHTTP2_PROTOCOL_ERROR, @@ -60,6 +62,7 @@ enum ErrorType { OGHTTP2_INADEQUATE_SECURITY, OGHTTP2_HTTP_1_1_REQUIRED, }; +// NOLINTEND(readability-identifier-naming) class Http2CodecImplTestFixture; @@ -73,12 +76,12 @@ class ReceivedSettingsImpl : public ReceivedSettings { explicit ReceivedSettingsImpl(absl::Span settings); // ReceivedSettings - const absl::optional& maxConcurrentStreams() const override { + const std::optional& maxConcurrentStreams() const override { return concurrent_stream_limit_; } private: - absl::optional concurrent_stream_limit_{}; + std::optional concurrent_stream_limit_; }; class Utility { @@ -151,7 +154,8 @@ class ConnectionImpl : public virtual Connection, ConnectionImpl(Network::Connection& connection, CodecStats& stats, Random::RandomGenerator& random_generator, const envoy::config::core::v3::Http2ProtocolOptions& http2_options, - const uint32_t max_headers_kb, const uint32_t max_headers_count); + const uint32_t max_headers_kb, const uint32_t max_headers_count, + OptRef runtime = std::nullopt); ~ConnectionImpl() override; @@ -330,7 +334,7 @@ class ConnectionImpl : public virtual Connection, void encodeData(Buffer::Instance& data, bool end_stream) override; Stream& getStream() override { return *this; } void encodeMetadata(const MetadataMapVector& metadata_map_vector) override; - Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() override { return absl::nullopt; } + Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() override { return std::nullopt; } // Http::Stream void addCallbacks(StreamCallbacks& callbacks) override { addCallbacksHelper(callbacks); } @@ -344,9 +348,9 @@ class ConnectionImpl : public virtual Connection, absl::string_view responseDetails() override { return details_; } Buffer::BufferMemoryAccountSharedPtr account() const override { return buffer_memory_account_; } void setAccount(Buffer::BufferMemoryAccountSharedPtr account) override; - absl::optional codecStreamId() const override { + std::optional codecStreamId() const override { if (stream_id_ == -1) { - return absl::nullopt; + return std::nullopt; } return stream_id_; } @@ -434,21 +438,23 @@ class ConnectionImpl : public virtual Connection, HeaderMapPtr pending_trailers_to_encode_; std::unique_ptr metadata_decoder_; std::unique_ptr metadata_encoder_; - absl::optional deferred_reset_; + std::optional deferred_reset_; // Holds the reset reason for this stream. Useful if we have buffered data // to determine whether we should continue processing that data. - absl::optional reset_reason_; + std::optional reset_reason_; HeaderString cookies_; - bool local_end_stream_sent_ : 1; - bool remote_end_stream_ : 1; - bool remote_rst_ : 1; - bool data_deferred_ : 1; - bool received_noninformational_headers_ : 1; - bool pending_receive_buffer_high_watermark_called_ : 1; - bool pending_send_buffer_high_watermark_called_ : 1; - bool reset_due_to_messaging_error_ : 1; + uint32_t cookie_count_; + bool local_end_stream_sent_ : 1 = false; + bool remote_end_stream_ : 1 = false; + bool remote_rst_ : 1 = false; + bool data_deferred_ : 1 = false; + bool received_noninformational_headers_ : 1 = false; + bool pending_receive_buffer_high_watermark_called_ : 1 = false; + bool pending_send_buffer_high_watermark_called_ : 1 = false; + bool reset_due_to_messaging_error_ : 1 = false; // Latch whether this stream is operating with this flag. - bool extend_stream_lifetime_flag_ : 1; + bool extend_stream_lifetime_flag_ : 1 = false; + bool histograms_recorded_ : 1 = false; absl::string_view details_; /** @@ -653,6 +659,7 @@ class ConnectionImpl : public virtual Connection, const StreamImpl* getStreamUnchecked(int32_t stream_id) const; StreamImpl* getStreamUnchecked(int32_t stream_id); int saveHeader(int32_t stream_id, HeaderString&& name, HeaderString&& value); + void recordHistogramsForStream(StreamImpl& stream); /** * Copies any frames pending internally by nghttp2 into outbound buffer. @@ -693,8 +700,8 @@ class ConnectionImpl : public virtual Connection, * `common_http_protocol_options.headers_with_underscores_action` configuration option in the * HttpConnectionManager. */ - virtual absl::optional checkHeaderNameForUnderscores(absl::string_view /* header_name */) { - return absl::nullopt; + virtual std::optional checkHeaderNameForUnderscores(absl::string_view /* header_name */) { + return std::nullopt; } /** @@ -721,7 +728,7 @@ class ConnectionImpl : public virtual Connection, // Tracks the stream id of the current stream we're processing. // This should only be set while we're in the context of dispatching to nghttp2. - absl::optional current_stream_id_; + std::optional current_stream_id_; std::unique_ptr visitor_; std::unique_ptr adapter_; @@ -733,6 +740,8 @@ class ConnectionImpl : public virtual Connection, bool allow_metadata_; uint64_t max_metadata_size_; const bool stream_error_on_invalid_http_messaging_; + const bool record_http2_histograms_; + const uint64_t max_cookie_size_bytes_{0}; // Status for any errors encountered by the nghttp2 callbacks. // nghttp2 library uses single return code to indicate callback failure and @@ -808,8 +817,8 @@ class ConnectionImpl : public virtual Connection, // remove streams from the map when they are closed in order to avoid calls to resetStreamWorker // after the stream has been removed from the active list. std::map pending_deferred_reset_streams_; - bool dispatching_ : 1; - bool raised_goaway_ : 1; + bool dispatching_ : 1 = false; + bool raised_goaway_ : 1 = false; Event::SchedulableCallbackPtr protocol_constraint_violation_callback_; Random::RandomGenerator& random_; MonotonicTime last_received_data_time_; @@ -859,14 +868,15 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { const uint32_t max_request_headers_count, envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action, - Server::OverloadManager& overload_manager); + Server::OverloadManager& overload_manager, + OptRef runtime = std::nullopt); private: // ConnectionImpl ConnectionCallbacks& callbacks() override { return callbacks_; } Status onBeginHeaders(int32_t stream_id) override; int onHeader(int32_t stream_id, HeaderString&& name, HeaderString&& value) override; - absl::optional checkHeaderNameForUnderscores(absl::string_view header_name) override; + std::optional checkHeaderNameForUnderscores(absl::string_view header_name) override; StreamResetReason getMessagingErrorResetReason() const override { return StreamResetReason::LocalReset; } @@ -885,6 +895,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { // The action to take when a request header name contains underscore characters. envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action_; + // Remove when removing runtime feature `http2_fix_goaway_loadshed_point`. Server::LoadShedPoint* should_send_go_away_on_dispatch_{nullptr}; Server::LoadShedPoint* should_send_go_away_and_close_on_dispatch_{nullptr}; bool sent_go_away_on_dispatch_{false}; diff --git a/source/common/http/http2/codec_stats.h b/source/common/http/http2/codec_stats.h index 2912cd05b1b5c..e70ab4b04a565 100644 --- a/source/common/http/http2/codec_stats.h +++ b/source/common/http/http2/codec_stats.h @@ -13,10 +13,12 @@ namespace Http2 { /** * All stats for the HTTP/2 codec. @see stats_macros.h */ -#define ALL_HTTP2_CODEC_STATS(COUNTER, GAUGE) \ +#define ALL_HTTP2_CODEC_STATS(COUNTER, GAUGE, HISTOGRAM) \ + COUNTER(cookies_total_bytes_too_large) \ COUNTER(dropped_headers_with_underscores) \ COUNTER(goaway_sent) \ COUNTER(header_overflow) \ + COUNTER(header_list_size_too_large) \ COUNTER(headers_cb_no_stream) \ COUNTER(inbound_empty_frames_flood) \ COUNTER(inbound_priority_frames_flood) \ @@ -36,7 +38,12 @@ namespace Http2 { GAUGE(pending_send_bytes, Accumulate) \ GAUGE(deferred_stream_close, Accumulate) \ GAUGE(outbound_frames_active, Accumulate) \ - GAUGE(outbound_control_frames_active, Accumulate) + GAUGE(outbound_control_frames_active, Accumulate) \ + HISTOGRAM(cookie_count, Unspecified) \ + HISTOGRAM(cookie_size, Bytes) \ + HISTOGRAM(header_count, Unspecified) \ + HISTOGRAM(header_list_size, Bytes) +#define GENERATE_CONSTRUCTOR_HISTOGRAM_PARAM(NAME, ...) Envoy::Stats::Histogram &NAME, /** * Wrapper struct for the HTTP/2 codec stats. @see stats_macros.h */ @@ -44,14 +51,17 @@ struct CodecStats : public ::Envoy::Http::HeaderValidatorStats { using AtomicPtr = Thread::AtomicPtr; CodecStats(ALL_HTTP2_CODEC_STATS(GENERATE_CONSTRUCTOR_COUNTER_PARAM, - GENERATE_CONSTRUCTOR_GAUGE_PARAM)...) + GENERATE_CONSTRUCTOR_GAUGE_PARAM, + GENERATE_CONSTRUCTOR_HISTOGRAM_PARAM)...) : ::Envoy::Http::HeaderValidatorStats() - ALL_HTTP2_CODEC_STATS(GENERATE_CONSTRUCTOR_INIT_LIST, GENERATE_CONSTRUCTOR_INIT_LIST) {} + ALL_HTTP2_CODEC_STATS(GENERATE_CONSTRUCTOR_INIT_LIST, GENERATE_CONSTRUCTOR_INIT_LIST, + GENERATE_CONSTRUCTOR_INIT_LIST) {} static CodecStats& atomicGet(AtomicPtr& ptr, Stats::Scope& scope) { return *ptr.get([&scope]() -> CodecStats* { return new CodecStats{ALL_HTTP2_CODEC_STATS(POOL_COUNTER_PREFIX(scope, "http2."), - POOL_GAUGE_PREFIX(scope, "http2."))}; + POOL_GAUGE_PREFIX(scope, "http2."), + POOL_HISTOGRAM_PREFIX(scope, "http2."))}; }); } @@ -61,7 +71,7 @@ struct CodecStats : public ::Envoy::Http::HeaderValidatorStats { } void incMessagingError() override { rx_messaging_error_.inc(); } - ALL_HTTP2_CODEC_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) + ALL_HTTP2_CODEC_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, GENERATE_HISTOGRAM_STRUCT) }; } // namespace Http2 diff --git a/source/common/http/http2/conn_pool.cc b/source/common/http/http2/conn_pool.cc index cf011d669b57d..6e4c704d28cc7 100644 --- a/source/common/http/http2/conn_pool.cc +++ b/source/common/http/http2/conn_pool.cc @@ -16,7 +16,7 @@ namespace Http2 { uint32_t ActiveClient::calculateInitialStreamsLimit( Http::HttpServerPropertiesCacheSharedPtr http_server_properties_cache, - absl::optional& origin, + std::optional& origin, Upstream::HostDescriptionConstSharedPtr host) { uint32_t initial_streams = host->cluster().httpProtocolOptions().http2Options().max_concurrent_streams().value(); @@ -57,12 +57,12 @@ allocateConnPool(Event::Dispatcher& dispatcher, Random::RandomGenerator& random_ const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, Upstream::ClusterConnectivityState& state, Server::OverloadManager& overload_manager, - absl::optional origin, + std::optional origin, Http::HttpServerPropertiesCacheSharedPtr cache) { return std::make_unique( host, priority, dispatcher, options, transport_socket_options, random_generator, state, [](HttpConnPoolImplBase* pool) { - return std::make_unique(*pool, absl::nullopt); + return std::make_unique(*pool, std::nullopt); }, [](Upstream::Host::CreateConnectionData& data, HttpConnPoolImplBase* pool) { CodecClientPtr codec{new CodecClientProd( diff --git a/source/common/http/http2/conn_pool.h b/source/common/http/http2/conn_pool.h index 35e3a4e787ff4..b9a9b24207151 100644 --- a/source/common/http/http2/conn_pool.h +++ b/source/common/http/http2/conn_pool.h @@ -22,7 +22,7 @@ class ActiveClient : public MultiplexedActiveClientBase { // configuration and cached SETTINGS. static uint32_t calculateInitialStreamsLimit( Http::HttpServerPropertiesCacheSharedPtr http_server_properties_cache, - absl::optional& origin, + std::optional& origin, Upstream::HostDescriptionConstSharedPtr host); ActiveClient(Envoy::Http::HttpConnPoolImplBase& parent, @@ -36,7 +36,7 @@ allocateConnPool(Event::Dispatcher& dispatcher, Random::RandomGenerator& random_ const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, Upstream::ClusterConnectivityState& state, Server::OverloadManager& overload_manager, - absl::optional origin = absl::nullopt, + std::optional origin = std::nullopt, Http::HttpServerPropertiesCacheSharedPtr http_server_properties_cache = nullptr); } // namespace Http2 diff --git a/source/common/http/http2/protocol_constraints.cc b/source/common/http/http2/protocol_constraints.cc index 367a7bb18ee56..091bce5810fcd 100644 --- a/source/common/http/http2/protocol_constraints.cc +++ b/source/common/http/http2/protocol_constraints.cc @@ -8,7 +8,8 @@ namespace Http { namespace Http2 { ProtocolConstraints::ProtocolConstraints( - CodecStats& stats, const envoy::config::core::v3::Http2ProtocolOptions& http2_options) + CodecStats& stats, const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + bool use_active_streams_for_limits) : stats_(stats), max_outbound_frames_(http2_options.max_outbound_frames().value()), frame_buffer_releasor_([this]() { releaseOutboundFrame(); }), max_outbound_control_frames_(http2_options.max_outbound_control_frames().value()), @@ -18,7 +19,8 @@ ProtocolConstraints::ProtocolConstraints( max_inbound_priority_frames_per_stream_( http2_options.max_inbound_priority_frames_per_stream().value()), max_inbound_window_update_frames_per_data_frame_sent_( - http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()) {} + http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()), + use_active_streams_for_limits_(use_active_streams_for_limits) {} ProtocolConstraints::ReleasorProc ProtocolConstraints::incrementOutboundFrameCount(bool is_outbound_flood_monitored_control_frame) { @@ -101,13 +103,14 @@ Status ProtocolConstraints::checkInboundFrameLimits() { } if (inbound_priority_frames_ > - static_cast(max_inbound_priority_frames_per_stream_) * (1 + opened_streams_)) { + static_cast(max_inbound_priority_frames_per_stream_) * + (1 + (use_active_streams_for_limits_ ? active_streams_ : opened_streams_))) { stats_.inbound_priority_frames_flood_.inc(); return bufferFloodError("Too many PRIORITY frames"); } if (inbound_window_update_frames_ > - 5 + 2 * (opened_streams_ + + 5 + 2 * ((use_active_streams_for_limits_ ? active_streams_ : opened_streams_) + max_inbound_window_update_frames_per_data_frame_sent_ * outbound_data_frames_)) { stats_.inbound_window_update_frames_flood_.inc(); return bufferFloodError("Too many WINDOW_UPDATE frames"); @@ -124,7 +127,8 @@ void ProtocolConstraints::dumpState(std::ostream& os, int indent_level) const { << DUMP_MEMBER(max_outbound_control_frames_) << DUMP_MEMBER(consecutive_inbound_frames_with_empty_payload_) << DUMP_MEMBER(max_consecutive_inbound_frames_with_empty_payload_) - << DUMP_MEMBER(opened_streams_) << DUMP_MEMBER(inbound_priority_frames_) + << DUMP_MEMBER(opened_streams_) << DUMP_MEMBER(active_streams_) + << DUMP_MEMBER(inbound_priority_frames_) << DUMP_MEMBER(max_inbound_priority_frames_per_stream_) << DUMP_MEMBER(inbound_window_update_frames_) << DUMP_MEMBER(outbound_data_frames_) << DUMP_MEMBER(max_inbound_window_update_frames_per_data_frame_sent_) << '\n'; diff --git a/source/common/http/http2/protocol_constraints.h b/source/common/http/http2/protocol_constraints.h index 86367d6d956cd..8dfdd8785a6b3 100644 --- a/source/common/http/http2/protocol_constraints.h +++ b/source/common/http/http2/protocol_constraints.h @@ -18,6 +18,7 @@ namespace Http { namespace Http2 { // Frame types as inherited from nghttp2 and preserved for oghttp2 +// NOLINTBEGIN(readability-identifier-naming) enum FrameType { OGHTTP2_DATA_FRAME_TYPE, OGHTTP2_HEADERS_FRAME_TYPE, @@ -30,6 +31,7 @@ enum FrameType { OGHTTP2_WINDOW_UPDATE_FRAME_TYPE, OGHTTP2_CONTINUATION_FRAME_TYPE, }; +// NOLINTEND(readability-identifier-naming) // Class for detecting abusive peers and validating additional constraints imposed by Envoy. // This class does not check protocol compliance with the H/2 standard, as this is checked by @@ -43,7 +45,8 @@ class ProtocolConstraints : public ScopeTrackedObject { using ReleasorProc = std::function; explicit ProtocolConstraints(CodecStats& stats, - const envoy::config::core::v3::Http2ProtocolOptions& http2_options); + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + bool use_active_streams_for_limits); // Return ok status if no protocol constraints were violated. // Return error status of the first detected violation. Subsequent violations of constraints @@ -66,7 +69,28 @@ class ProtocolConstraints : public ScopeTrackedObject { Status trackInboundFrame(uint8_t type, bool end_stream, bool is_empty); // Increment the number of DATA frames sent to the peer. void incrementOutboundDataFrameCount() { ++outbound_data_frames_; } - void incrementOpenedStreamCount() { ++opened_streams_; } + void incrementOpenedStreamCount() { + ++opened_streams_; + ++active_streams_; + } + void decrementActiveStreamCount() { + ASSERT(active_streams_ > 0); + if (active_streams_ > 0) { + --active_streams_; + if (use_active_streams_for_limits_) { + if (inbound_priority_frames_ > max_inbound_priority_frames_per_stream_) { + inbound_priority_frames_ -= max_inbound_priority_frames_per_stream_; + } else { + inbound_priority_frames_ = 0; + } + if (inbound_window_update_frames_ > 2) { + inbound_window_update_frames_ -= 2; + } else { + inbound_window_update_frames_ = 0; + } + } + } + } Status checkOutboundFrameLimits(); @@ -113,6 +137,8 @@ class ProtocolConstraints : public ScopeTrackedObject { // For upstream connections this is incremented when the first HEADERS frame with the new // stream ID is sent to the upstream server. uint32_t opened_streams_ = 0; + // This counter keeps track of the number of currently active streams. + uint32_t active_streams_ = 0; // This counter keeps track of the number of inbound PRIORITY frames. If this counter exceeds // the value calculated using this formula: // @@ -137,6 +163,8 @@ class ProtocolConstraints : public ScopeTrackedObject { // Maximum number of inbound WINDOW_UPDATE frames per outbound DATA frame sent. Initialized // from corresponding http2_protocol_options. Default value is 10. const uint32_t max_inbound_window_update_frames_per_data_frame_sent_; + + const bool use_active_streams_for_limits_; }; } // namespace Http2 diff --git a/source/common/http/http3/conn_pool.cc b/source/common/http/http3/conn_pool.cc index 9063110cda578..f0449e31aa7c5 100644 --- a/source/common/http/http3/conn_pool.cc +++ b/source/common/http/http3/conn_pool.cc @@ -70,7 +70,9 @@ ActiveClient::ActiveClient(Envoy::Http::HttpConnPoolImplBase& parent, return; } codec_client_->connect(); - if (readyForStream()) { + if (!Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.fix_http3_early_data_timing") && + readyForStream()) { // This client can send early data, so check if there are any pending streams can be sent // as early data. parent_.onUpstreamReadyForEarlyData(*this); @@ -90,9 +92,14 @@ void ActiveClient::onMaxStreamsChanged(uint32_t num_streams) { parent_.transitionActiveClientState(*this, ActiveClient::State::Ready); // If there's waiting streams, make sure the pool will now serve them. parent_.onUpstreamReady(); - } else if (currentUnusedCapacity() == 0 && state() == ActiveClient::State::ReadyForEarlyData) { - // With HTTP/3 this can only happen during a rejected 0-RTT handshake. - parent_.transitionActiveClientState(*this, ActiveClient::State::Busy); + } else if (state() == ActiveClient::State::ReadyForEarlyData) { + if (currentUnusedCapacity() == 0) { + // With HTTP/3 this can only happen during a rejected 0-RTT handshake. + parent_.transitionActiveClientState(*this, ActiveClient::State::Busy); + } else if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.fix_http3_early_data_timing")) { + parent_.onUpstreamReadyForEarlyData(*this); + } } } @@ -123,14 +130,14 @@ Http3ConnPoolImpl::Http3ConnPoolImpl( network_observer_registry_(network_observer_registry) {} void Http3ConnPoolImpl::onConnected(Envoy::ConnectionPool::ActiveClient&) { - if (connect_callback_ != absl::nullopt) { + if (connect_callback_ != std::nullopt) { connect_callback_->onHandshakeComplete(); } } void Http3ConnPoolImpl::onConnectFailed(Envoy::ConnectionPool::ActiveClient& client) { ASSERT(client.numActiveStreams() == 0); - if (static_cast(client).hasCreatedStream() && connect_callback_ != absl::nullopt) { + if (static_cast(client).hasCreatedStream() && connect_callback_ != std::nullopt) { connect_callback_->onZeroRttHandshakeFailed(); } } diff --git a/source/common/http/http_server_properties_cache_impl.cc b/source/common/http/http_server_properties_cache_impl.cc index 7a23c11fe74bc..654f4d158d105 100644 --- a/source/common/http/http_server_properties_cache_impl.cc +++ b/source/common/http/http_server_properties_cache_impl.cc @@ -28,13 +28,13 @@ HttpServerPropertiesCacheImpl::originToString(const HttpServerPropertiesCache::O return absl::StrCat(origin.scheme_, "://", origin.hostname_, ":", origin.port_); } -absl::optional +std::optional HttpServerPropertiesCacheImpl::stringToOrigin(const std::string& str) { const re2::RE2& origin_regex = ConstRegexHolder::get().origin_regex; std::string scheme; std::string hostname; int port = 0; - if (re2::RE2::FullMatch(str.c_str(), origin_regex, &scheme, &hostname, &port)) { + if (re2::RE2::FullMatch(str, origin_regex, &scheme, &hostname, &port)) { return HttpServerPropertiesCache::Origin(scheme, hostname, port); } return {}; @@ -62,7 +62,7 @@ std::string HttpServerPropertiesCacheImpl::originDataToStringForCache(const Orig return value; } -absl::optional +std::optional HttpServerPropertiesCacheImpl::originDataFromString(absl::string_view origin_data_string, TimeSource& time_source, bool from_cache) { const std::vector parts = absl::StrSplit(origin_data_string, '|'); @@ -124,9 +124,9 @@ HttpServerPropertiesCacheImpl::HttpServerPropertiesCacheImpl( if (key_value_store) { KeyValueStore::ConstIterateCb load_protocols = [this](const std::string& key, const std::string& value) { - absl::optional origin_data = + std::optional origin_data = originDataFromString(value, dispatcher_.timeSource(), true); - absl::optional origin = stringToOrigin(key); + std::optional origin = stringToOrigin(key); if (origin_data.has_value() && origin.has_value()) { // We deferred transfering ownership into key_value_store_ prior, so // that we won't end up doing redundant updates to the store while @@ -158,7 +158,7 @@ void HttpServerPropertiesCacheImpl::setAlternatives(const Origin& origin, auto it = setPropertiesImpl(origin, data); if (key_value_store_) { key_value_store_->addOrUpdate(originToString(origin), originDataToStringForCache(it->second), - absl::nullopt); + std::nullopt); } } @@ -168,7 +168,7 @@ void HttpServerPropertiesCacheImpl::setSrtt(const Origin& origin, std::chrono::m auto it = setPropertiesImpl(origin, data); if (key_value_store_) { key_value_store_->addOrUpdate(originToString(origin), originDataToStringForCache(it->second), - absl::nullopt); + std::nullopt); } } @@ -179,7 +179,7 @@ std::chrono::microseconds HttpServerPropertiesCacheImpl::getSrtt(const Origin& o return entry_it->second.srtt; } if (use_canonical_suffix) { - absl::optional canonical = getCanonicalOrigin(origin.hostname_); + std::optional canonical = getCanonicalOrigin(origin.hostname_); if (canonical.has_value()) { entry_it = protocols_.find(*canonical); if (entry_it != protocols_.end()) { @@ -197,7 +197,7 @@ void HttpServerPropertiesCacheImpl::setConcurrentStreams(const Origin& origin, auto it = setPropertiesImpl(origin, data); if (key_value_store_) { key_value_store_->addOrUpdate(originToString(origin), originDataToStringForCache(it->second), - absl::nullopt); + std::nullopt); } } @@ -262,7 +262,7 @@ OptRef> HttpServerPropertiesCacheImpl::findAlternatives(const Origin& origin) { auto entry_it = protocols_.find(origin); if (entry_it == protocols_.end() || !entry_it->second.protocols.has_value()) { - absl::optional canonical = getCanonicalOrigin(origin.hostname_); + std::optional canonical = getCanonicalOrigin(origin.hostname_); if (canonical.has_value()) { entry_it = protocols_.find(*canonical); } @@ -288,7 +288,7 @@ HttpServerPropertiesCacheImpl::findAlternatives(const Origin& origin) { } if (key_value_store_ && original_size != protocols.size()) { key_value_store_->addOrUpdate(originToString(origin), - originDataToStringForCache(entry_it->second), absl::nullopt); + originDataToStringForCache(entry_it->second), std::nullopt); } return makeOptRef(const_cast&>(protocols)); } @@ -331,7 +331,7 @@ bool HttpServerPropertiesCacheImpl::isHttp3Broken(const Origin& origin) { return entry_it->second.h3_status_tracker->isHttp3Broken(); } - absl::optional canonical = getCanonicalOriginForHttp3Brokenness(origin.hostname_); + std::optional canonical = getCanonicalOriginForHttp3Brokenness(origin.hostname_); if (!canonical.has_value()) { return false; } @@ -346,7 +346,7 @@ bool HttpServerPropertiesCacheImpl::isHttp3Broken(const Origin& origin) { return false; } -absl::optional +std::optional HttpServerPropertiesCacheImpl::getCanonicalOriginForHttp3Brokenness(absl::string_view hostname) { absl::string_view suffix = getCanonicalSuffix(hostname); if (suffix.empty()) { @@ -395,7 +395,7 @@ HttpServerPropertiesCacheImpl::getCanonicalSuffix(absl::string_view hostname) co return ""; } -absl::optional +std::optional HttpServerPropertiesCacheImpl::getCanonicalOrigin(absl::string_view hostname) const { absl::string_view suffix = getCanonicalSuffix(hostname); if (suffix.empty()) { diff --git a/source/common/http/http_server_properties_cache_impl.h b/source/common/http/http_server_properties_cache_impl.h index 56aaf3bd9ab43..d00989fb1ac87 100644 --- a/source/common/http/http_server_properties_cache_impl.h +++ b/source/common/http/http_server_properties_cache_impl.h @@ -43,7 +43,7 @@ class HttpServerPropertiesCacheImpl : public HttpServerPropertiesCache, concurrent_streams(concurrent_streams) {} // The alternate protocols supported if available. - absl::optional> protocols; + std::optional> protocols; // The last smoothed round trip time, if available else 0. std::chrono::microseconds srtt; // The last connectivity status of HTTP/3, if available else nullptr. @@ -55,7 +55,7 @@ class HttpServerPropertiesCacheImpl : public HttpServerPropertiesCache, // Converts an Origin to a string which can be parsed by stringToOrigin. static std::string originToString(const HttpServerPropertiesCache::Origin& origin); // Converts a string from originToString back to structured format. - static absl::optional stringToOrigin(const std::string& str); + static std::optional stringToOrigin(const std::string& str); // Convert origin data to a string to cache to the key value // store. Note that in order to determine the lifetime of entries, this @@ -66,13 +66,13 @@ class HttpServerPropertiesCacheImpl : public HttpServerPropertiesCache, // The string format is: // protocols|rtt static std::string originDataToStringForCache(const OriginData& data); - // Parse an origin data into structured data, or absl::nullopt + // Parse an origin data into structured data, or std::nullopt // if it is empty or invalid. // If from_cache is true, it is assumed the string was serialized using // protocolsToStringForCache and the the ma fields will be parsed as absolute times // rather than relative time. - static absl::optional originDataFromString(absl::string_view origin_data, - TimeSource& time_source, bool from_cache); + static std::optional originDataFromString(absl::string_view origin_data, + TimeSource& time_source, bool from_cache); // Parse an alt-svc string into a vector of structured data. // If from_cache is true, it is assumed the string was serialized using // protocolsToStringForCache and the the ma fields will be parsed as absolute times @@ -142,13 +142,13 @@ class HttpServerPropertiesCacheImpl : public HttpServerPropertiesCache, // Returns the canonical origin from the canonical_h3_broken_map, if any, associated with // `hostname`. - absl::optional getCanonicalOriginForHttp3Brokenness(absl::string_view hostname); + std::optional getCanonicalOriginForHttp3Brokenness(absl::string_view hostname); // Updates the canonical origin for http3 brokenness book keeping. void maybeSetCanonicalOriginForHttp3Brokenness(const Origin& origin); // Returns the canonical origin, if any, associated with `hostname`. - absl::optional getCanonicalOrigin(absl::string_view hostname) const; + std::optional getCanonicalOrigin(absl::string_view hostname) const; // If `origin` matches a canonical suffix then updates canonical_alt_svc_map_ accordingly. void maybeSetCanonicalOrigin(const Origin& origin); diff --git a/source/common/http/http_service_headers.h b/source/common/http/http_service_headers.h index fc236f8c20831..fa7cf1cd5ef64 100644 --- a/source/common/http/http_service_headers.h +++ b/source/common/http/http_service_headers.h @@ -1,7 +1,7 @@ #pragma once #include "envoy/config/core/v3/http_service.pb.h" -#include "envoy/formatter/substitution_formatter_base.h" +#include "envoy/formatter/substitution_formatter.h" #include "envoy/http/header_map.h" #include "envoy/server/factory_context.h" diff --git a/source/common/http/matching/data_impl.h b/source/common/http/matching/data_impl.h index 243c2a831c47c..a9cfeed3718ea 100644 --- a/source/common/http/matching/data_impl.h +++ b/source/common/http/matching/data_impl.h @@ -16,61 +16,11 @@ namespace Matching { /** * Implementation of HttpMatchingData, providing HTTP specific data to * the match tree. + * + * This using declaration will be removed, once all references have been migrated to + * HttpMatchingData. */ -class HttpMatchingDataImpl : public HttpMatchingData { -public: - explicit HttpMatchingDataImpl(const StreamInfo::StreamInfo& stream_info) - : stream_info_(stream_info) {} - - static absl::string_view name() { return "http"; } - - void onRequestHeaders(const RequestHeaderMap& request_headers) { - request_headers_ = &request_headers; - } - - void onRequestTrailers(const RequestTrailerMap& request_trailers) { - request_trailers_ = &request_trailers; - } - - void onResponseHeaders(const ResponseHeaderMap& response_headers) { - response_headers_ = &response_headers; - } - - void onResponseTrailers(const ResponseTrailerMap& response_trailers) { - response_trailers_ = &response_trailers; - } - - RequestHeaderMapOptConstRef requestHeaders() const override { - return makeOptRefFromPtr(request_headers_); - } - - RequestTrailerMapOptConstRef requestTrailers() const override { - return makeOptRefFromPtr(request_trailers_); - } - - ResponseHeaderMapOptConstRef responseHeaders() const override { - return makeOptRefFromPtr(response_headers_); - } - - ResponseTrailerMapOptConstRef responseTrailers() const override { - return makeOptRefFromPtr(response_trailers_); - } - - const StreamInfo::StreamInfo& streamInfo() const override { return stream_info_; } - - const Network::ConnectionInfoProvider& connectionInfoProvider() const override { - return stream_info_.downstreamAddressProvider(); - } - -private: - const StreamInfo::StreamInfo& stream_info_; - const RequestHeaderMap* request_headers_{}; - const ResponseHeaderMap* response_headers_{}; - const RequestTrailerMap* request_trailers_{}; - const ResponseTrailerMap* response_trailers_{}; -}; - -using HttpMatchingDataImplSharedPtr = std::shared_ptr; +using HttpMatchingDataImpl = HttpMatchingData; struct HttpFilterActionContext { // Identify whether the filter is in downstream filter chain or upstream filter chain. diff --git a/source/common/http/mixed_conn_pool.cc b/source/common/http/mixed_conn_pool.cc index 7869073697a6d..03014c227760f 100644 --- a/source/common/http/mixed_conn_pool.cc +++ b/source/common/http/mixed_conn_pool.cc @@ -14,7 +14,7 @@ Envoy::ConnectionPool::ActiveClientPtr HttpConnPoolImplMixed::instantiateActiveC uint32_t initial_streams = Http2::ActiveClient::calculateInitialStreamsLimit( http_server_properties_cache_, origin_, host()); return std::make_unique( - *this, Envoy::ConnectionPool::ConnPoolImplBase::host(), initial_streams, absl::nullopt); + *this, Envoy::ConnectionPool::ConnPoolImplBase::host(), initial_streams, std::nullopt); } CodecClientPtr @@ -33,7 +33,7 @@ void HttpConnPoolImplMixed::onConnected(Envoy::ConnectionPool::ActiveClient& cli // HTTP client is associated with that connection. When the first call returns, the // Network::Connection will inform the new callback (the HTTP client) that it // is connected. The early return is to ignore that second call. - if (client.protocol() != absl::nullopt) { + if (client.protocol() != std::nullopt) { return; } diff --git a/source/common/http/mixed_conn_pool.h b/source/common/http/mixed_conn_pool.h index 4716329d2dbc9..28b3f6b8ccc27 100644 --- a/source/common/http/mixed_conn_pool.h +++ b/source/common/http/mixed_conn_pool.h @@ -17,7 +17,7 @@ class HttpConnPoolImplMixed : public HttpConnPoolImplBase { const Network::ConnectionSocket::OptionsSharedPtr& options, const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, Upstream::ClusterConnectivityState& state, - absl::optional origin, + std::optional origin, Http::HttpServerPropertiesCacheSharedPtr http_server_properties_cache, Server::OverloadManager& overload_manager) : HttpConnPoolImplBase(std::move(host), std::move(priority), dispatcher, options, @@ -37,7 +37,7 @@ class HttpConnPoolImplMixed : public HttpConnPoolImplBase { // Default to HTTP/1, as servers which don't support ALPN are probably HTTP/1 only. Http::Protocol protocol_ = Protocol::Http11; Http::HttpServerPropertiesCacheSharedPtr http_server_properties_cache_; - absl::optional origin_; + std::optional origin_; }; } // namespace Http diff --git a/source/common/http/muxdemux.cc b/source/common/http/muxdemux.cc index 8d62962a7a503..1d092e20f7352 100644 --- a/source/common/http/muxdemux.cc +++ b/source/common/http/muxdemux.cc @@ -148,7 +148,7 @@ void MultiStream::maybeSwitchToIdle() { MuxDemux::MuxDemux(Server::Configuration::FactoryContext& context) : factory_context_(context) {} -MuxDemux::~MuxDemux() {} +MuxDemux::~MuxDemux() = default; absl::StatusOr> MuxDemux::multicast(const AsyncClient::StreamOptions& options, diff --git a/source/common/http/muxdemux.h b/source/common/http/muxdemux.h index 1dd8c3b6892b7..c26549b081c9b 100644 --- a/source/common/http/muxdemux.h +++ b/source/common/http/muxdemux.h @@ -50,10 +50,11 @@ class MultiStream { // Iterator over streams. Allows sending different headers, body or trailers to different streams. struct StreamIterator { - using difference_type = std::ptrdiff_t; - using element_type = AsyncClient::Stream*; - using pointer = element_type*; - using reference = element_type&; + // Standard iterator aliases intentionally use STL-prescribed snake_case names. + using difference_type = std::ptrdiff_t; // NOLINT(readability-identifier-naming) + using element_type = AsyncClient::Stream*; // NOLINT(readability-identifier-naming) + using pointer = element_type*; // NOLINT(readability-identifier-naming) + using reference = element_type&; // NOLINT(readability-identifier-naming) explicit StreamIterator(std::vector::iterator it) : it(it) {} StreamIterator() = default; @@ -112,7 +113,7 @@ class MuxDemux : public std::enable_shared_from_this { struct Callbacks { std::string cluster_name; std::weak_ptr callbacks; - absl::optional options; + std::optional options; }; static std::shared_ptr create(Server::Configuration::FactoryContext& context) { diff --git a/source/common/http/null_route_impl.h b/source/common/http/null_route_impl.h index f057d788efa99..5c828e558a164 100644 --- a/source/common/http/null_route_impl.h +++ b/source/common/http/null_route_impl.h @@ -91,7 +91,7 @@ struct NullPathMatchCriterion : public Router::PathMatchCriterion { struct RouteEntryImpl : public Router::RouteEntry { static absl::StatusOr> - create(const std::string& cluster_name, const absl::optional& timeout, + create(const std::string& cluster_name, const std::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, @@ -106,7 +106,7 @@ struct RouteEntryImpl : public Router::RouteEntry { protected: RouteEntryImpl( - const std::string& cluster_name, const absl::optional& timeout, + const std::string& cluster_name, const std::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, @@ -172,22 +172,20 @@ struct RouteEntryImpl : public Router::RouteEntry { } } bool usingNewTimeouts() const override { return false; } - absl::optional idleTimeout() const override { return absl::nullopt; } - absl::optional flushTimeout() const override { return absl::nullopt; } - absl::optional maxStreamDuration() const override { - return absl::nullopt; + std::optional idleTimeout() const override { return std::nullopt; } + std::optional flushTimeout() const override { return std::nullopt; } + std::optional maxStreamDuration() const override { + return std::nullopt; } - absl::optional grpcTimeoutHeaderMax() const override { - return absl::nullopt; + std::optional grpcTimeoutHeaderMax() const override { + return std::nullopt; } - absl::optional grpcTimeoutHeaderOffset() const override { - return absl::nullopt; + std::optional grpcTimeoutHeaderOffset() const override { + return std::nullopt; } - absl::optional maxGrpcTimeout() const override { - return absl::nullopt; - } - absl::optional grpcTimeoutOffset() const override { - return absl::nullopt; + std::optional maxGrpcTimeout() const override { return std::nullopt; } + std::optional grpcTimeoutOffset() const override { + return std::nullopt; } const Router::TlsContextMatchCriteria* tlsContextMatchCriteria() const override { return nullptr; @@ -226,7 +224,7 @@ struct RouteEntryImpl : public Router::RouteEntry { Router::RouteEntry::UpgradeMap upgrade_map_; const std::string cluster_name_; - absl::optional timeout_; + std::optional timeout_; static const ConnectConfigOptRef connect_config_nullopt_; // Pass early data option config through StreamOptions. std::unique_ptr early_data_policy_{ @@ -236,7 +234,7 @@ struct RouteEntryImpl : public Router::RouteEntry { struct NullRouteImpl : public Router::Route { static absl::StatusOr> create(const std::string cluster_name, Router::RetryPolicyConstSharedPtr retry_policy, - Regex::Engine& regex_engine, const absl::optional& timeout = {}, + Regex::Engine& regex_engine, const std::optional& timeout = {}, const Protobuf::RepeatedPtrField& hash_policy = {}, const Router::MetadataMatchCriteria* metadata_match = nullptr) { @@ -266,7 +264,7 @@ struct NullRouteImpl : public Router::Route { const Envoy::Config::TypedMetadata& typedMetadata() const override { return Router::DefaultRouteMetadataPack::get().typed_metadata_; } - absl::optional filterDisabled(absl::string_view) const override { return {}; } + std::optional filterDisabled(absl::string_view) const override { return {}; } const std::string& routeName() const override { return EMPTY_STRING; } const Router::VirtualHost& virtualHost() const override { return *virtual_host_; } Router::VirtualHostConstSharedPtr virtualHostSharedPtr() const override { return virtual_host_; } @@ -277,7 +275,7 @@ struct NullRouteImpl : public Router::Route { protected: NullRouteImpl(const std::string cluster_name, Router::RetryPolicyConstSharedPtr retry_policy, Regex::Engine& regex_engine, - const absl::optional& timeout, + const std::optional& timeout, const Protobuf::RepeatedPtrField& hash_policy, absl::Status& creation_status, diff --git a/source/common/http/path_utility.cc b/source/common/http/path_utility.cc index c37b7473e25e9..702adba28c7a7 100644 --- a/source/common/http/path_utility.cc +++ b/source/common/http/path_utility.cc @@ -1,11 +1,12 @@ #include "source/common/http/path_utility.h" +#include + #include "source/common/common/logger.h" #include "absl/strings/str_join.h" #include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" -#include "absl/types/optional.h" #include "url/url_canon.h" #include "url/url_canon_stdstring.h" @@ -13,16 +14,16 @@ namespace Envoy { namespace Http { namespace { -absl::optional canonicalizePath(absl::string_view original_path) { +std::optional canonicalizePath(absl::string_view original_path) { std::string canonical_path; url::Component in_component(0, original_path.size()); url::Component out_component; url::StdStringCanonOutput output(&canonical_path); if (!url::CanonicalizePath(original_path.data(), in_component, &output, &out_component)) { - return absl::nullopt; + return std::nullopt; } output.Complete(); - return absl::make_optional(std::move(canonical_path)); + return std::make_optional(std::move(canonical_path)); } } // namespace diff --git a/source/common/http/route_config_update_requster.cc b/source/common/http/route_config_update_requster.cc index a0a3c8924c711..8a0e2d2eade61 100644 --- a/source/common/http/route_config_update_requster.cc +++ b/source/common/http/route_config_update_requster.cc @@ -6,7 +6,7 @@ namespace Http { // TODO(chaoqin-li1123): Make on demand vhds and on demand srds works at the same time. void RdsRouteConfigUpdateRequester::requestRouteConfigUpdate( RouteCache& route_cache, Http::RouteConfigUpdatedCallbackSharedPtr route_config_updated_cb, - absl::optional route_config, Event::Dispatcher& dispatcher, + std::optional route_config, Event::Dispatcher& dispatcher, RequestHeaderMap& request_headers) { if (route_config.has_value() && route_config.value()->usesVhds()) { ASSERT(!request_headers.Host()->value().empty()); diff --git a/source/common/http/route_config_update_requster.h b/source/common/http/route_config_update_requster.h index 740908e1f4a68..a9ce557002e41 100644 --- a/source/common/http/route_config_update_requster.h +++ b/source/common/http/route_config_update_requster.h @@ -25,7 +25,7 @@ class RdsRouteConfigUpdateRequester : public RouteConfigUpdateRequester { void requestRouteConfigUpdate(RouteCache& route_cache, Http::RouteConfigUpdatedCallbackSharedPtr route_config_updated_cb, - absl::optional route_config, + std::optional route_config, Event::Dispatcher& dispatcher, RequestHeaderMap& request_headers) override; void @@ -45,9 +45,7 @@ class RdsRouteConfigUpdateRequester : public RouteConfigUpdateRequester { class RdsRouteConfigUpdateRequesterFactory : public RouteConfigUpdateRequesterFactory { public: // UntypedFactory - virtual std::string name() const override { - return "envoy.route_config_update_requester.default"; - } + std::string name() const override { return "envoy.route_config_update_requester.default"; } std::unique_ptr createRouteConfigUpdateRequester(Router::RouteConfigProvider* route_config_provider) override { diff --git a/source/common/http/session_idle_list.h b/source/common/http/session_idle_list.h index 8c00f3f7b0cb6..16dd68887df83 100644 --- a/source/common/http/session_idle_list.h +++ b/source/common/http/session_idle_list.h @@ -30,24 +30,30 @@ class SessionIdleList : public SessionIdleListInterface, public Logger::Loggable ~SessionIdleList() override = default; // Adds a session to the idle list. + // NOLINTNEXTLINE(readability-identifier-naming) void AddSession(IdleSessionInterface& session) override; // Removes a session from the idle list. + // NOLINTNEXTLINE(readability-identifier-naming) void RemoveSession(IdleSessionInterface& session) override; // Terminates idle sessions if they are eligible for termination. This is // called by the worker thread when the system is overloaded. + // NOLINTNEXTLINE(readability-identifier-naming) void MaybeTerminateIdleSessions(bool is_saturated) override; // Sets the minimum time before a session can be terminated. + // NOLINTNEXTLINE(readability-identifier-naming) void set_min_time_before_termination_allowed(absl::Duration min_time_before_termination_allowed) { min_time_before_termination_allowed_ = min_time_before_termination_allowed; }; + // NOLINTNEXTLINE(readability-identifier-naming) void set_max_sessions_to_terminate_in_one_round(int max_sessions_to_terminate_in_one_round) { max_sessions_to_terminate_in_one_round_ = max_sessions_to_terminate_in_one_round; } + // NOLINTNEXTLINE(readability-identifier-naming) void set_max_sessions_to_terminate_in_one_round_when_saturated( int max_sessions_to_terminate_in_one_round_when_saturated) { max_sessions_to_terminate_in_one_round_when_saturated_ = @@ -55,6 +61,7 @@ class SessionIdleList : public SessionIdleListInterface, public Logger::Loggable } // Sets whether to ignore the minimum time before a session can be terminated. + // NOLINTNEXTLINE(readability-identifier-naming) void set_ignore_min_time_before_termination_allowed(bool ignore) { ignore_min_time_before_termination_allowed_ = ignore; }; @@ -88,16 +95,21 @@ class SessionIdleList : public SessionIdleListInterface, public Logger::Loggable IdleSessions(const IdleSessions&) = delete; IdleSessions& operator=(const IdleSessions&) = delete; + // NOLINTNEXTLINE(readability-identifier-naming) IdleSessionInterface& next_session_to_terminate() { return *set_.begin()->session; } + // NOLINTNEXTLINE(readability-identifier-naming) void AddSessionToList(MonotonicTime enqueue_time, IdleSessionInterface& session); + // NOLINTNEXTLINE(readability-identifier-naming) void RemoveSessionFromList(IdleSessionInterface& session); // Get the time at which the session was added to the idle list. + // NOLINTNEXTLINE(readability-identifier-naming) MonotonicTime GetEnqueueTime(IdleSessionInterface& session) const; // Returns true if the session is in the map. For testing only. + // NOLINTNEXTLINE(readability-identifier-naming) bool ContainsForTest(IdleSessionInterface& session) const { return map_.contains(&session); } size_t size() const { return set_.size(); } @@ -111,14 +123,17 @@ class SessionIdleList : public SessionIdleListInterface, public Logger::Loggable IdleSessionMap map_; }; + // NOLINTNEXTLINE(readability-identifier-naming) const IdleSessions* idle_sessions() const { return &idle_sessions_; } // If this is > 0 then we do not terminate more than that many // sessions in a single attempt. This prevents us from doing too // much work in a single round. We want a small constant for this. + // NOLINTNEXTLINE(readability-identifier-naming) size_t MaxSessionsToTerminateInOneRound(bool is_saturated) const; // Returns the minimum time before a session can be terminated. + // NOLINTNEXTLINE(readability-identifier-naming) absl::Duration MinTimeBeforeTerminationAllowed() const; Event::Dispatcher& dispatcher_; diff --git a/source/common/http/session_idle_list_interface.h b/source/common/http/session_idle_list_interface.h index d0e13451f0aab..938f7fe83f83c 100644 --- a/source/common/http/session_idle_list_interface.h +++ b/source/common/http/session_idle_list_interface.h @@ -10,6 +10,7 @@ class IdleSessionInterface { // Terminates the idle session. This is called by the SessionIdleList when // the system is overloaded and the session is eligible for termination. + // NOLINTNEXTLINE(readability-identifier-naming) virtual void TerminateIdleSession() = 0; }; @@ -19,13 +20,16 @@ class SessionIdleListInterface { virtual ~SessionIdleListInterface() = default; // Adds a session to the idle list. + // NOLINTNEXTLINE(readability-identifier-naming) virtual void AddSession(IdleSessionInterface& session) = 0; // Removes a session from the idle list. + // NOLINTNEXTLINE(readability-identifier-naming) virtual void RemoveSession(IdleSessionInterface& session) = 0; // Terminates idle sessions if they are eligible for termination. This is // called by the worker thread when the system is overloaded. + // NOLINTNEXTLINE(readability-identifier-naming) virtual void MaybeTerminateIdleSessions(bool is_saturated) = 0; }; diff --git a/source/common/http/sse/BUILD b/source/common/http/sse/BUILD index 9b716b80be24b..a9d7243d63712 100644 --- a/source/common/http/sse/BUILD +++ b/source/common/http/sse/BUILD @@ -14,6 +14,5 @@ envoy_cc_library( hdrs = ["sse_parser.h"], deps = [ "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/types:optional", ], ) diff --git a/source/common/http/sse/sse_parser.h b/source/common/http/sse/sse_parser.h index 2f9ae664414a2..76f0c132122a3 100644 --- a/source/common/http/sse/sse_parser.h +++ b/source/common/http/sse/sse_parser.h @@ -1,12 +1,12 @@ #pragma once #include +#include #include #include #include #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Http { @@ -48,15 +48,15 @@ class SseParser { */ struct ParsedEvent { // The concatenated data field values. Per SSE spec, multiple data fields are joined with - // newlines. absl::nullopt if no data fields present, empty string if data field exists but + // newlines. std::nullopt if no data fields present, empty string if data field exists but // empty. - absl::optional data; - // The event ID. absl::nullopt if no id field is present. - absl::optional id; - // The event type. absl::nullopt if no event field is present. - absl::optional event_type; - // The reconnection time in milliseconds. absl::nullopt if no retry field is present. - absl::optional retry; + std::optional data; + // The event ID. std::nullopt if no id field is present. + std::optional id; + // The event type. std::nullopt if no event field is present. + std::optional event_type; + // The reconnection time in milliseconds. std::nullopt if no retry field is present. + std::optional retry; }; /** diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 7a207abd0911a..89c69186e13a0 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -33,7 +34,6 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" #include "quiche/http2/adapter/http2_protocol.h" namespace Envoy { @@ -552,13 +552,13 @@ void Utility::QueryParamsMulti::overwrite(absl::string_view key, absl::string_vi this->data_[key] = std::vector{std::string(value)}; } -absl::optional Utility::QueryParamsMulti::getFirstValue(absl::string_view key) const { +std::optional Utility::QueryParamsMulti::getFirstValue(absl::string_view key) const { auto it = this->data_.find(key); if (it == this->data_.end()) { return std::nullopt; } - return absl::optional{it->second.at(0)}; + return std::optional{it->second.at(0)}; } absl::string_view Utility::findQueryStringStart(const HeaderString& path) { @@ -656,11 +656,11 @@ uint64_t Utility::getResponseStatus(const ResponseHeaderMap& headers) { return status.value(); } -absl::optional Utility::getResponseStatusOrNullopt(const ResponseHeaderMap& headers) { +std::optional Utility::getResponseStatusOrNullopt(const ResponseHeaderMap& headers) { const HeaderEntry* header = headers.Status(); uint64_t response_code; if (!header || !absl::SimpleAtoi(headers.getStatusValue(), &response_code)) { - return absl::nullopt; + return std::nullopt; } return response_code; } @@ -1070,7 +1070,7 @@ const std::string& Utility::getProtocolString(const Protocol protocol) { } std::string Utility::buildOriginalUri(const Http::RequestHeaderMap& request_headers, - const absl::optional max_path_length) { + const std::optional max_path_length) { if (!request_headers.Path()) { return ""; } @@ -1188,6 +1188,8 @@ const std::string Utility::resetReasonToString(const Http::StreamResetReason res return "overload manager reset"; case Http::StreamResetReason::Http1PrematureUpstreamHalfClose: return "HTTP/1 premature upstream half close"; + case Http::StreamResetReason::RemoteResetNoError: + return "remote reset (no error)"; } return ""; @@ -1324,41 +1326,17 @@ namespace { // %-encode all ASCII character codepoints, EXCEPT: // ALPHA | DIGIT | * | - | . | _ // SPACE is encoded as %20, NOT as the + character -constexpr std::array kUrlEncodedCharTable = { - // control characters - 0b11111111111111111111111111111111, - // !"#$%&'()*+,-./0123456789:;<=>? - 0b11111111110110010000000000111111, - //@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ - 0b10000000000000000000000000011110, - //`abcdefghijklmnopqrstuvwxyz{|}~ - 0b10000000000000000000000000011111, - // extended ascii - 0b11111111111111111111111111111111, - 0b11111111111111111111111111111111, - 0b11111111111111111111111111111111, - 0b11111111111111111111111111111111, -}; +constexpr CharTable kUrlEncodedCharTable = + ~(CharTables::kAlphanumeric | CharTable::fromChars("*-._")); -constexpr std::array kUrlDecodedCharTable = { - // control characters - 0b00000000000000000000000000000000, - // !"#$%&'()*+,-./0123456789:;<=>? - 0b01011111111111111111111111110101, - //@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ - 0b11111111111111111111111111110101, - //`abcdefghijklmnopqrstuvwxyz{|}~ - 0b11111111111111111111111111100010, - // extended ascii - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, - 0b00000000000000000000000000000000, -}; +// The set of characters which, if they are percent-encoded, should be +// decoded. +constexpr CharTable kUrlDecodedCharTable = + CharTables::kAlphanumeric | CharTable::fromChars("!#$%&'()*+,-./:;=?@[]_`~"); -bool shouldPercentEncodeChar(char c) { return testCharInTable(kUrlEncodedCharTable, c); } +constexpr bool shouldPercentEncodeChar(char c) { return kUrlEncodedCharTable.hasChar(c); } -bool shouldPercentDecodeChar(char c) { return testCharInTable(kUrlDecodedCharTable, c); } +constexpr bool shouldPercentDecodeChar(char c) { return kUrlDecodedCharTable.hasChar(c); } } // namespace std::string Utility::PercentEncoding::urlEncode(absl::string_view value) { @@ -1430,7 +1408,7 @@ Utility::AuthorityAttributes Utility::parseAuthority(absl::string_view host) { // effort attempt. const auto colon_pos = host.rfind(':'); absl::string_view host_to_resolve = host; - absl::optional port; + std::optional port; if (colon_pos != absl::string_view::npos && host_to_resolve.back() != ']') { const absl::string_view string_view_host = host; host_to_resolve = string_view_host.substr(0, colon_pos); @@ -1641,6 +1619,30 @@ std::string Utility::newUri(::Envoy::OptRef redir return fmt::format("{}://{}{}{}", final_scheme, final_host, final_port, final_path); } +std::string Utility::newUriWithFormatter(OptRef redirect_config, + const Http::RequestHeaderMap& headers, + const Formatter::Formatter& formatter, + const StreamInfo::StreamInfo& stream_info) { + const Formatter::Context context(&headers); + const std::string formatted_path = formatter.format(context, stream_info); + if (!formatted_path.empty()) { + const RedirectConfig path_redirect_config{ + redirect_config ? redirect_config->scheme_redirect_ : "", + redirect_config ? redirect_config->host_redirect_ : "", + redirect_config ? redirect_config->port_redirect_ : "", + formatted_path, + "", + "", + nullptr, + nullptr, + formatted_path.find('?') != std::string::npos, + redirect_config ? redirect_config->https_redirect_ : false, + redirect_config ? redirect_config->strip_query_ : false}; + return newUri(makeOptRef(path_redirect_config), headers); + } + return newUri(redirect_config, headers); +} + bool Utility::isValidRefererValue(absl::string_view value) { // First, we try to parse it as an absolute URL and @@ -1671,7 +1673,7 @@ bool Utility::isValidRefererValue(absl::string_view value) { seen_slash = true; continue; default: - if (!testCharInTable(kUriQueryAndFragmentCharTable, c)) { + if (!CharTables::kUriQueryAndFragment.hasChar(c)) { return false; } } diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 73716add98be2..dd23f6f73bf13 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -11,19 +12,20 @@ #include "envoy/config/core/v3/http_uri.pb.h" #include "envoy/config/core/v3/protocol.pb.h" #include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/formatter/substitution_formatter.h" #include "envoy/grpc/status.h" #include "envoy/http/codes.h" #include "envoy/http/filter.h" #include "envoy/http/message.h" #include "envoy/http/metadata_interface.h" #include "envoy/http/query_params.h" +#include "envoy/stream_info/stream_info.h" #include "source/common/http/exception.h" #include "source/common/http/http_option_limits.h" #include "source/common/http/status.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Http { @@ -294,9 +296,9 @@ uint64_t getResponseStatus(const ResponseHeaderMap& headers); /** * Get the response status from the response headers. * @param headers supplies the headers to get the status from. - * @return absl::optional the response code or absl::nullopt if the headers are invalid. + * @return std::optional the response code or std::nullopt if the headers are invalid. */ -absl::optional getResponseStatusOrNullopt(const ResponseHeaderMap& headers); +std::optional getResponseStatusOrNullopt(const ResponseHeaderMap& headers); /** * Determine whether these headers are a valid Upgrade request or response. @@ -359,7 +361,7 @@ struct LocalReplyData { // Supplies the optional body text which is returned. absl::string_view body_text_; // gRPC status code to override the httpToGrpcStatus mapping with. - const absl::optional grpc_status_; + const std::optional grpc_status_; // Tells if this is a response to a HEAD request. bool is_head_request_ = false; }; @@ -468,7 +470,7 @@ const std::string& getProtocolString(const Protocol p); * @param length to truncate the constructed URI's path */ std::string buildOriginalUri(const Http::RequestHeaderMap& request_headers, - absl::optional max_path_length); + std::optional max_path_length); /** * Extract scheme, host and path from a URI. The host may contain a port. @@ -640,7 +642,7 @@ struct AuthorityAttributes { absl::string_view host_; // If parsed authority has port, that is stored here. - absl::optional port_; + std::optional port_; }; /** @@ -697,6 +699,7 @@ struct RedirectConfig { const std::string prefix_rewrite_redirect_; const std::string regex_rewrite_redirect_substitution_; Regex::CompiledMatcherPtr regex_rewrite_redirect_; + Formatter::FormatterPtr path_rewrite_formatter_; // Keep small members (bools and enums) at the end of class, to reduce alignment overhead. const bool path_redirect_has_query_; const bool https_redirect_; @@ -728,6 +731,15 @@ bool schemeIsHttps(const absl::string_view scheme); std::string newUri(::Envoy::OptRef redirect_config, const Http::RequestHeaderMap& headers); +/* + * Compute new URI, evaluating formatter to derive the path. Falls back to newUri if the formatter + * produces an empty path. + */ +std::string newUriWithFormatter(OptRef redirect_config, + const Http::RequestHeaderMap& headers, + const Formatter::Formatter& formatter, + const StreamInfo::StreamInfo& stream_info); + } // namespace Utility } // namespace Http } // namespace Envoy diff --git a/source/common/io/io_uring_impl.cc b/source/common/io/io_uring_impl.cc index 8096646c3fff7..4c0fb8116cf05 100644 --- a/source/common/io/io_uring_impl.cc +++ b/source/common/io/io_uring_impl.cc @@ -2,9 +2,106 @@ #include +#include + namespace Envoy { namespace Io { +namespace { +// The buffer group id for the provided buffer ring backing `multishot` reads. A single group is +// enough since each worker thread owns its own io_uring. +constexpr uint16_t ProvidedBufferGroupId = 0; + +// Idle time in milliseconds before the SQPOLL kernel thread sleeps when the submission queue is +// empty. +constexpr uint32_t SqPollIdleMs = 100; + +// Rounds the configured ring size up to the next power of two, as required by the provided buffer +// ring, capped so the number of provided buffers stays bounded. +uint32_t providedBufferCount(uint32_t io_uring_size) { + constexpr uint32_t MaxProvidedBuffers = 4096; + uint32_t count = 1; + while (count < io_uring_size && count < MaxProvidedBuffers) { + count <<= 1; + } + return count; +} +} // namespace + +// A provided buffer ring registered with the kernel. The buffer memory is owned here and kept alive +// through the shared_ptr held by read fragments, while the kernel ring is released by the owning +// IoUringImpl before the io_uring is torn down. +class IoUringBufferPoolImpl : public IoUringBufferPool { +public: + IoUringBufferPoolImpl(struct io_uring& ring, uint16_t group_id, uint32_t buffer_count, + uint32_t buffer_size) + : ring_(&ring), group_id_(group_id), buffer_count_(buffer_count), buffer_size_(buffer_size), + mask_(io_uring_buf_ring_mask(buffer_count)) { + int err = 0; + buf_ring_ = io_uring_setup_buf_ring(ring_, buffer_count_, group_id_, 0, &err); + if (buf_ring_ == nullptr) { + return; + } + // The kernel ring is registered, so allocate the backing memory and hand every buffer to it. + buffers_ = std::make_unique(static_cast(buffer_count_) * buffer_size_); + // The buffer id is a 16-bit field in io_uring, and buffer_count_ is capped well below that by + // providedBufferCount. Iterate with the same width as buffer_count_ so the loop is bounded even + // if the cap ever grows. + for (uint32_t id = 0; id < buffer_count_; id++) { + io_uring_buf_ring_add(buf_ring_, getBuffer(id), buffer_size_, static_cast(id), + mask_, static_cast(id)); + } + io_uring_buf_ring_advance(buf_ring_, static_cast(buffer_count_)); + } + + // Whether the kernel buffer ring was registered successfully. False on kernels that lack provided + // buffer ring support. + bool valid() const { return buf_ring_ != nullptr; } + + // Releases the kernel buffer ring while the io_uring is still alive. After this releaseBuffer is + // a no-op, so outstanding read fragments can be drained safely once the ring is gone. + void releaseRing() { + if (buf_ring_ != nullptr) { + io_uring_free_buf_ring(ring_, buf_ring_, buffer_count_, group_id_); + buf_ring_ = nullptr; + } + } + + uint16_t groupId() const { return group_id_; } + + // IoUringBufferPool + uint8_t* getBuffer(uint32_t buffer_id) override { + return buffers_.get() + static_cast(buffer_id) * buffer_size_; + } + uint32_t bufferSize() const override { return buffer_size_; } + void releaseBuffer(const void* buffer) override { + if (buf_ring_ == nullptr) { + return; + } + // A stray pointer would index the ring out of bounds, so reject anything outside the backing + // memory. + const uint8_t* const buf_ptr = static_cast(buffer); + const uint8_t* const start = buffers_.get(); + const uint8_t* const end = start + static_cast(buffer_count_) * buffer_size_; + if (buf_ptr < start || buf_ptr >= end) { + IS_ENVOY_BUG("released buffer pointer is out of range"); + return; + } + const auto buffer_id = static_cast((buf_ptr - start) / buffer_size_); + io_uring_buf_ring_add(buf_ring_, const_cast(buffer), buffer_size_, buffer_id, mask_, 0); + io_uring_buf_ring_advance(buf_ring_, 1); + } + +private: + struct io_uring* const ring_; + struct io_uring_buf_ring* buf_ring_{nullptr}; + const uint16_t group_id_; + const uint32_t buffer_count_; + const uint32_t buffer_size_; + const int mask_; + std::unique_ptr buffers_; +}; + bool isIoUringSupported() { struct io_uring_params p {}; struct io_uring ring; @@ -17,20 +114,61 @@ bool isIoUringSupported() { return is_supported; } -IoUringImpl::IoUringImpl(uint32_t io_uring_size, bool use_submission_queue_polling) - : cqes_(io_uring_size, nullptr) { +IoUringImpl::IoUringImpl(uint32_t io_uring_size, bool use_submission_queue_polling, + bool enable_multishot_receive, uint32_t multishot_buffer_size) { struct io_uring_params p {}; + + // Size the completion queue at twice the submission queue to reduce the chance of overflow. + p.flags |= IORING_SETUP_CQSIZE; + p.cq_entries = io_uring_size * 2; + if (use_submission_queue_polling) { p.flags |= IORING_SETUP_SQPOLL; + p.sq_thread_idle = SqPollIdleMs; } - // TODO (soulxu): According to the man page: `By default, the CQ ring will have twice the number - // of entries as specified by entries for the SQ ring`. But currently we only use the same size - // with SQ ring. We will figure out better handle of entries number in the future. + int ret = io_uring_queue_init_params(io_uring_size, &ring_, &p); + if (ret == -EINVAL) { + // `IORING_SETUP_CQSIZE` requires kernel 5.5 or newer. Retry without it on older kernels. + p.flags &= ~static_cast(IORING_SETUP_CQSIZE); + p.cq_entries = 0; + ret = io_uring_queue_init_params(io_uring_size, &ring_, &p); + } RELEASE_ASSERT(ret == 0, fmt::format("unable to initialize io_uring: {}", errorDetails(-ret))); + + // Size the completion vector to the submission queue, not the larger completion queue. Each read + // completion re-arms one read, so reaping at most one submission queue worth of completions per + // pass keeps those read re-arms within the submission queue. A larger burst is reaped across + // later passes, which the worker re-arms while hasReadyCompletions stays true. + cqes_.resize(io_uring_size, nullptr); + + // Set up the provided buffer ring for `multishot` reads when requested. A failure here means the + // running kernel lacks provided buffer ring support, in which case `multishot` reads stay + // disabled and the readv-based read path is used instead. + if (enable_multishot_receive && multishot_buffer_size > 0) { + const uint32_t buffer_count = providedBufferCount(io_uring_size); + auto pool = std::make_shared(ring_, ProvidedBufferGroupId, buffer_count, + multishot_buffer_size); + if (pool->valid()) { + buffer_pool_ = std::move(pool); + ENVOY_LOG(debug, "io_uring multishot reads enabled, {} buffers of {} bytes", buffer_count, + multishot_buffer_size); + } else { + ENVOY_LOG(debug, "io_uring multishot reads requested but provided buffer rings are " + "unsupported, falling back to readv"); + } + } } -IoUringImpl::~IoUringImpl() { io_uring_queue_exit(&ring_); } +IoUringImpl::~IoUringImpl() { + // Release the provided buffer ring while the io_uring is still alive. The pool object itself may + // outlive this ring when read fragments still reference its buffers, after which releaseBuffer is + // a no-op. + if (buffer_pool_ != nullptr) { + buffer_pool_->releaseRing(); + } + io_uring_queue_exit(&ring_); +} os_fd_t IoUringImpl::registerEventfd() { ASSERT(!isEventfdRegistered()); @@ -53,6 +191,16 @@ void IoUringImpl::unregisterEventfd() { bool IoUringImpl::isEventfdRegistered() const { return SOCKET_VALID(event_fd_); } +void IoUringImpl::checkCqOverflow() { + if (*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW) { + cq_overflow_count_++; + // Overflow persists under heavy load, so rate limit the warning to avoid flooding the log. + ENVOY_LOG_PERIODIC(warn, std::chrono::seconds(1), + "io_uring completion queue overflow detected, count = {}", + cq_overflow_count_); + } +} + void IoUringImpl::forEveryCompletion(const CompletionCb& completion_cb) { ASSERT(SOCKET_VALID(event_fd_)); @@ -65,13 +213,21 @@ void IoUringImpl::forEveryCompletion(const CompletionCb& completion_cb) { } } + checkCqOverflow(); unsigned count = io_uring_peek_batch_cqe(&ring_, cqes_.data(), cqes_.size()); - for (unsigned i = 0; i < count; ++i) { struct io_uring_cqe* cqe = cqes_[i]; - completion_cb(reinterpret_cast(cqe->user_data), cqe->res, false); + auto* req = reinterpret_cast(cqe->user_data); + if (req != nullptr) { + // Surface the `multishot` completion metadata so the read path can recycle the provided + // buffer and keep the request alive while the operation stays armed. + req->setMoreCompletions((cqe->flags & IORING_CQE_F_MORE) != 0); + req->setBufferId((cqe->flags & IORING_CQE_F_BUFFER) + ? static_cast(cqe->flags >> IORING_CQE_BUFFER_SHIFT) + : -1); + } + completion_cb(req, cqe->res, false); } - io_uring_cq_advance(&ring_, count); ENVOY_LOG(trace, "the num of injected completion is {}", injected_completions_.size()); @@ -79,22 +235,17 @@ void IoUringImpl::forEveryCompletion(const CompletionCb& completion_cb) { // long. // Iterate the injected completion. while (!injected_completions_.empty()) { - auto& completion = injected_completions_.front(); - completion_cb(completion.user_data_, completion.result_, true); - // The socket may closed in the completion_cb and all the related completions are - // removed. - if (injected_completions_.empty()) { - break; - } + auto completion = injected_completions_.front(); injected_completions_.pop_front(); + completion_cb(completion.user_data_, completion.result_, true); } } +bool IoUringImpl::hasReadyCompletions() const { return io_uring_cq_ready(&ring_) > 0; } + IoUringResult IoUringImpl::prepareAccept(os_fd_t fd, struct sockaddr* remote_addr, socklen_t* remote_addr_len, Request* user_data) { - ENVOY_LOG(trace, "prepare close for fd = {}", fd); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); + ENVOY_LOG(trace, "prepare accept for fd = {}", fd); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { return IoUringResult::Failed; @@ -109,8 +260,6 @@ IoUringResult IoUringImpl::prepareConnect(os_fd_t fd, const Network::Address::InstanceConstSharedPtr& address, Request* user_data) { ENVOY_LOG(trace, "prepare connect for fd = {}", fd); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { return IoUringResult::Failed; @@ -124,8 +273,6 @@ IoUringResult IoUringImpl::prepareConnect(os_fd_t fd, IoUringResult IoUringImpl::prepareReadv(os_fd_t fd, const struct iovec* iovecs, unsigned nr_vecs, off_t offset, Request* user_data) { ENVOY_LOG(trace, "prepare readv for fd = {}", fd); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { return IoUringResult::Failed; @@ -136,11 +283,28 @@ IoUringResult IoUringImpl::prepareReadv(os_fd_t fd, const struct iovec* iovecs, return IoUringResult::Ok; } +bool IoUringImpl::isMultishotEnabled() const { return buffer_pool_ != nullptr; } + +IoUringBufferPoolSharedPtr IoUringImpl::bufferPool() { return buffer_pool_; } + +IoUringResult IoUringImpl::prepareReadMultishot(os_fd_t fd, Request* user_data) { + ENVOY_LOG(trace, "prepare read multishot for fd = {}", fd); + ASSERT(buffer_pool_ != nullptr); + struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); + if (sqe == nullptr) { + return IoUringResult::Failed; + } + + io_uring_prep_recv_multishot(sqe, fd, nullptr, 0, 0); + sqe->flags |= IOSQE_BUFFER_SELECT; + io_uring_sqe_set_buf_group(sqe, buffer_pool_->groupId()); + io_uring_sqe_set_data(sqe, user_data); + return IoUringResult::Ok; +} + IoUringResult IoUringImpl::prepareWritev(os_fd_t fd, const struct iovec* iovecs, unsigned nr_vecs, off_t offset, Request* user_data) { ENVOY_LOG(trace, "prepare writev for fd = {}", fd); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { return IoUringResult::Failed; @@ -153,8 +317,6 @@ IoUringResult IoUringImpl::prepareWritev(os_fd_t fd, const struct iovec* iovecs, IoUringResult IoUringImpl::prepareClose(os_fd_t fd, Request* user_data) { ENVOY_LOG(trace, "prepare close for fd = {}", fd); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { return IoUringResult::Failed; @@ -166,9 +328,7 @@ IoUringResult IoUringImpl::prepareClose(os_fd_t fd, Request* user_data) { } IoUringResult IoUringImpl::prepareCancel(Request* cancelling_user_data, Request* user_data) { - ENVOY_LOG(trace, "prepare cancels for user data = {}", fmt::ptr(cancelling_user_data)); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); + ENVOY_LOG(trace, "prepare cancel for user data = {}", fmt::ptr(cancelling_user_data)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { ENVOY_LOG(trace, "failed to prepare cancel for user data = {}", fmt::ptr(cancelling_user_data)); @@ -182,8 +342,6 @@ IoUringResult IoUringImpl::prepareCancel(Request* cancelling_user_data, Request* IoUringResult IoUringImpl::prepareShutdown(os_fd_t fd, int how, Request* user_data) { ENVOY_LOG(trace, "prepare shutdown for fd = {}, how = {}", fd, how); - // TODO (soulxu): Handling the case of CQ ring is overflow. - ASSERT(!(*(ring_.sq.kflags) & IORING_SQ_CQ_OVERFLOW)); struct io_uring_sqe* sqe = io_uring_get_sqe(&ring_); if (sqe == nullptr) { ENVOY_LOG(trace, "failed to prepare shutdown for fd = {}", fd); diff --git a/source/common/io/io_uring_impl.h b/source/common/io/io_uring_impl.h index 196ba0d942a0e..e537a7bdc0cce 100644 --- a/source/common/io/io_uring_impl.h +++ b/source/common/io/io_uring_impl.h @@ -21,23 +21,30 @@ struct InjectedCompletion { const int32_t result_; }; +class IoUringBufferPoolImpl; + class IoUringImpl : public IoUring, public ThreadLocal::ThreadLocalObject, protected Logger::Loggable { public: - IoUringImpl(uint32_t io_uring_size, bool use_submission_queue_polling); + IoUringImpl(uint32_t io_uring_size, bool use_submission_queue_polling, + bool enable_multishot_receive, uint32_t multishot_buffer_size); ~IoUringImpl() override; os_fd_t registerEventfd() override; void unregisterEventfd() override; bool isEventfdRegistered() const override; void forEveryCompletion(const CompletionCb& completion_cb) override; + bool hasReadyCompletions() const override; IoUringResult prepareAccept(os_fd_t fd, struct sockaddr* remote_addr, socklen_t* remote_addr_len, Request* user_data) override; IoUringResult prepareConnect(os_fd_t fd, const Network::Address::InstanceConstSharedPtr& address, Request* user_data) override; IoUringResult prepareReadv(os_fd_t fd, const struct iovec* iovecs, unsigned nr_vecs, off_t offset, Request* user_data) override; + bool isMultishotEnabled() const override; + IoUringBufferPoolSharedPtr bufferPool() override; + IoUringResult prepareReadMultishot(os_fd_t fd, Request* user_data) override; IoUringResult prepareWritev(os_fd_t fd, const struct iovec* iovecs, unsigned nr_vecs, off_t offset, Request* user_data) override; IoUringResult prepareClose(os_fd_t fd, Request* user_data) override; @@ -48,10 +55,19 @@ class IoUringImpl : public IoUring, void removeInjectedCompletion(os_fd_t fd) override; private: + // Logs a warning when the completion queue has overflowed. The kernel parks the extra completions + // in a backlog and flushes them on the next submission, so they are reaped on a later pass. + void checkCqOverflow(); + struct io_uring ring_ {}; std::vector cqes_; os_fd_t event_fd_{INVALID_SOCKET}; std::list injected_completions_; + uint64_t cq_overflow_count_{0}; + // The provided buffer pool backing `multishot` reads. Null when `multishot` reads are disabled or + // not supported by the kernel. Held as a shared_ptr so read fragments can keep the buffer memory + // alive after this ring is gone. + std::shared_ptr buffer_pool_; }; } // namespace Io diff --git a/source/common/io/io_uring_worker_factory_impl.cc b/source/common/io/io_uring_worker_factory_impl.cc index b667032d32f2b..ac48b04fc9697 100644 --- a/source/common/io/io_uring_worker_factory_impl.cc +++ b/source/common/io/io_uring_worker_factory_impl.cc @@ -5,18 +5,19 @@ namespace Envoy { namespace Io { -IoUringWorkerFactoryImpl::IoUringWorkerFactoryImpl(uint32_t io_uring_size, - bool use_submission_queue_polling, - uint32_t read_buffer_size, - uint32_t write_timeout_ms, - ThreadLocal::SlotAllocator& tls) +IoUringWorkerFactoryImpl::IoUringWorkerFactoryImpl( + uint32_t io_uring_size, bool use_submission_queue_polling, bool enable_multishot_receive, + uint32_t read_buffer_size, uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, ThreadLocal::SlotAllocator& tls) : io_uring_size_(io_uring_size), use_submission_queue_polling_(use_submission_queue_polling), - read_buffer_size_(read_buffer_size), write_timeout_ms_(write_timeout_ms), tls_(tls) {} + enable_multishot_receive_(enable_multishot_receive), read_buffer_size_(read_buffer_size), + write_timeout_ms_(write_timeout_ms), write_high_watermark_bytes_(write_high_watermark_bytes), + write_low_watermark_bytes_(write_low_watermark_bytes), tls_(tls) {} OptRef IoUringWorkerFactoryImpl::getIoUringWorker() { auto ret = tls_.get(); - if (ret == absl::nullopt) { - return absl::nullopt; + if (ret == std::nullopt) { + return std::nullopt; } return ret; } @@ -24,10 +25,13 @@ OptRef IoUringWorkerFactoryImpl::getIoUringWorker() { void IoUringWorkerFactoryImpl::onWorkerThreadInitialized() { tls_.set([io_uring_size = io_uring_size_, use_submission_queue_polling = use_submission_queue_polling_, - read_buffer_size = read_buffer_size_, - write_timeout_ms = write_timeout_ms_](Event::Dispatcher& dispatcher) { - return std::make_shared(io_uring_size, use_submission_queue_polling, - read_buffer_size, write_timeout_ms, dispatcher); + enable_multishot_receive = enable_multishot_receive_, + read_buffer_size = read_buffer_size_, write_timeout_ms = write_timeout_ms_, + write_high_watermark_bytes = write_high_watermark_bytes_, + write_low_watermark_bytes = write_low_watermark_bytes_](Event::Dispatcher& dispatcher) { + return std::make_shared( + io_uring_size, use_submission_queue_polling, enable_multishot_receive, read_buffer_size, + write_timeout_ms, write_high_watermark_bytes, write_low_watermark_bytes, dispatcher); }); } diff --git a/source/common/io/io_uring_worker_factory_impl.h b/source/common/io/io_uring_worker_factory_impl.h index 03f5f66443542..7fbb04b2b81bf 100644 --- a/source/common/io/io_uring_worker_factory_impl.h +++ b/source/common/io/io_uring_worker_factory_impl.h @@ -9,8 +9,9 @@ namespace Io { class IoUringWorkerFactoryImpl : public IoUringWorkerFactory { public: IoUringWorkerFactoryImpl(uint32_t io_uring_size, bool use_submission_queue_polling, - uint32_t read_buffer_size, uint32_t write_timeout_ms, - ThreadLocal::SlotAllocator& tls); + bool enable_multishot_receive, uint32_t read_buffer_size, + uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, ThreadLocal::SlotAllocator& tls); OptRef getIoUringWorker() override; @@ -20,8 +21,11 @@ class IoUringWorkerFactoryImpl : public IoUringWorkerFactory { private: const uint32_t io_uring_size_; const bool use_submission_queue_polling_; + const bool enable_multishot_receive_; const uint32_t read_buffer_size_; const uint32_t write_timeout_ms_; + const uint32_t write_high_watermark_bytes_; + const uint32_t write_low_watermark_bytes_; ThreadLocal::TypedSlot tls_; }; diff --git a/source/common/io/io_uring_worker_impl.cc b/source/common/io/io_uring_worker_impl.cc index 80622775f65dd..36c2e203163d2 100644 --- a/source/common/io/io_uring_worker_impl.cc +++ b/source/common/io/io_uring_worker_impl.cc @@ -1,20 +1,39 @@ #include "source/common/io/io_uring_worker_impl.h" +#include + namespace Envoy { namespace Io { +// The adaptive read buffer grows up to this multiple of the configured read buffer size so large +// transfers use fewer, larger reads while small responses keep using small buffers. +constexpr uint32_t MaxReadBufferSizeMultiplier = 16; + +namespace { +// A negative provided buffer id would index the pool out of bounds. Flag the bug and let the caller +// skip the buffer when that happens. +bool multishotBufferIdValid(int32_t buffer_id) { + if (buffer_id < 0) { + IS_ENVOY_BUG(fmt::format("invalid multishot buffer id {}", buffer_id)); + return false; + } + return true; +} +} // namespace + ReadRequest::ReadRequest(IoUringSocket& socket, uint32_t size) - : Request(RequestType::Read, socket), buf_(std::make_unique(size)), - iov_(std::make_unique()) { - iov_->iov_base = buf_.get(); - iov_->iov_len = size; + // Value-initialize the buffer because io_uring fills it in the kernel, which MemorySanitizer + // cannot observe and would otherwise report as uninitialized. + : Request(RequestType::Read, socket), buf_(std::make_unique(size)) { + iov_.iov_base = buf_.get(); + iov_.iov_len = size; } WriteRequest::WriteRequest(IoUringSocket& socket, const Buffer::RawSliceVector& slices) - : Request(RequestType::Write, socket), iov_(std::make_unique(slices.size())) { - for (size_t i = 0; i < slices.size(); i++) { - iov_[i].iov_base = slices[i].mem_; - iov_[i].iov_len = slices[i].len_; + : Request(RequestType::Write, socket) { + iov_.reserve(slices.size()); + for (const auto& slice : slices) { + iov_.push_back({slice.mem_, slice.len_}); } } @@ -59,15 +78,23 @@ void IoUringSocketEntry::onRemoteClose() { } IoUringWorkerImpl::IoUringWorkerImpl(uint32_t io_uring_size, bool use_submission_queue_polling, - uint32_t read_buffer_size, uint32_t write_timeout_ms, + bool enable_multishot_receive, uint32_t read_buffer_size, + uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, Event::Dispatcher& dispatcher) - : IoUringWorkerImpl(std::make_unique(io_uring_size, use_submission_queue_polling), - read_buffer_size, write_timeout_ms, dispatcher) {} + : IoUringWorkerImpl(std::make_unique(io_uring_size, use_submission_queue_polling, + enable_multishot_receive, read_buffer_size), + read_buffer_size, write_timeout_ms, write_high_watermark_bytes, + write_low_watermark_bytes, dispatcher) {} IoUringWorkerImpl::IoUringWorkerImpl(IoUringPtr&& io_uring, uint32_t read_buffer_size, - uint32_t write_timeout_ms, Event::Dispatcher& dispatcher) - : io_uring_(std::move(io_uring)), read_buffer_size_(read_buffer_size), - write_timeout_ms_(write_timeout_ms), dispatcher_(dispatcher) { + uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, + Event::Dispatcher& dispatcher) + : io_uring_(std::move(io_uring)), multishot_enabled_(io_uring_->isMultishotEnabled()), + buffer_pool_(io_uring_->bufferPool()), read_buffer_size_(read_buffer_size), + write_timeout_ms_(write_timeout_ms), write_high_watermark_bytes_(write_high_watermark_bytes), + write_low_watermark_bytes_(write_low_watermark_bytes), dispatcher_(dispatcher) { const os_fd_t event_fd = io_uring_->registerEventfd(); // We only care about the read event of Eventfd, since we only receive the // event here. @@ -104,7 +131,8 @@ IoUringSocket& IoUringWorkerImpl::addServerSocket(os_fd_t fd, Event::FileReadyCb bool enable_close_event) { ENVOY_LOG(trace, "add server socket, fd = {}", fd); std::unique_ptr socket = std::make_unique( - fd, *this, std::move(cb), write_timeout_ms_, enable_close_event); + fd, *this, std::move(cb), write_timeout_ms_, write_high_watermark_bytes_, + write_low_watermark_bytes_, enable_close_event); socket->enableRead(); return addSocket(std::move(socket)); } @@ -113,7 +141,8 @@ IoUringSocket& IoUringWorkerImpl::addServerSocket(os_fd_t fd, Buffer::Instance& Event::FileReadyCb cb, bool enable_close_event) { ENVOY_LOG(trace, "add server socket through existing socket, fd = {}", fd); std::unique_ptr socket = std::make_unique( - fd, read_buf, *this, std::move(cb), write_timeout_ms_, enable_close_event); + fd, read_buf, *this, std::move(cb), write_timeout_ms_, write_high_watermark_bytes_, + write_low_watermark_bytes_, enable_close_event); socket->enableRead(); return addSocket(std::move(socket)); } @@ -123,7 +152,8 @@ IoUringSocket& IoUringWorkerImpl::addClientSocket(os_fd_t fd, Event::FileReadyCb ENVOY_LOG(trace, "add client socket, fd = {}", fd); // The client socket should not be read enabled until it is connected. std::unique_ptr socket = std::make_unique( - fd, *this, std::move(cb), write_timeout_ms_, enable_close_event); + fd, *this, std::move(cb), write_timeout_ms_, write_high_watermark_bytes_, + write_low_watermark_bytes_, enable_close_event); return addSocket(std::move(socket)); } @@ -153,32 +183,52 @@ IoUringWorkerImpl::submitConnectRequest(IoUringSocket& socket, } Request* IoUringWorkerImpl::submitReadRequest(IoUringSocket& socket) { - ReadRequest* req = new ReadRequest(socket, read_buffer_size_); + return submitReadRequest(socket, read_buffer_size_); +} + +Request* IoUringWorkerImpl::submitReadRequest(IoUringSocket& socket, uint32_t read_size) { + ReadRequest* req = new ReadRequest(socket, read_size); ENVOY_LOG(trace, "submit read request, fd = {}, read req = {}", socket.fd(), fmt::ptr(req)); - auto res = io_uring_->prepareReadv(socket.fd(), req->iov_.get(), 1, 0, req); + auto res = io_uring_->prepareReadv(socket.fd(), &req->iov_, 1, 0, req); if (res == IoUringResult::Failed) { // TODO(rojkov): handle `EBUSY` in case the completion queue is never reaped. submit(); - res = io_uring_->prepareReadv(socket.fd(), req->iov_.get(), 1, 0, req); + res = io_uring_->prepareReadv(socket.fd(), &req->iov_, 1, 0, req); RELEASE_ASSERT(res == IoUringResult::Ok, "unable to prepare readv"); } submit(); return req; } +Request* IoUringWorkerImpl::submitReadMultishotRequest(IoUringSocket& socket) { + Request* req = new Request(Request::RequestType::Read, socket); + + ENVOY_LOG(trace, "submit read multishot request, fd = {}, req = {}", socket.fd(), fmt::ptr(req)); + + auto res = io_uring_->prepareReadMultishot(socket.fd(), req); + if (res == IoUringResult::Failed) { + // TODO(rojkov): handle `EBUSY` in case the completion queue is never reaped. + submit(); + res = io_uring_->prepareReadMultishot(socket.fd(), req); + RELEASE_ASSERT(res == IoUringResult::Ok, "unable to prepare read multishot"); + } + submit(); + return req; +} + Request* IoUringWorkerImpl::submitWriteRequest(IoUringSocket& socket, const Buffer::RawSliceVector& slices) { WriteRequest* req = new WriteRequest(socket, slices); ENVOY_LOG(trace, "submit write request, fd = {}, req = {}", socket.fd(), fmt::ptr(req)); - auto res = io_uring_->prepareWritev(socket.fd(), req->iov_.get(), slices.size(), 0, req); + auto res = io_uring_->prepareWritev(socket.fd(), req->iov_.data(), slices.size(), 0, req); if (res == IoUringResult::Failed) { // TODO(rojkov): handle `EBUSY` in case the completion queue is never reaped. submit(); - res = io_uring_->prepareWritev(socket.fd(), req->iov_.get(), slices.size(), 0, req); + res = io_uring_->prepareWritev(socket.fd(), req->iov_.data(), slices.size(), 0, req); RELEASE_ASSERT(res == IoUringResult::Ok, "unable to prepare writev"); } submit(); @@ -229,7 +279,7 @@ Request* IoUringWorkerImpl::submitShutdownRequest(IoUringSocket& socket, int how // TODO(rojkov): handle `EBUSY` in case the completion queue is never reaped. submit(); res = io_uring_->prepareShutdown(socket.fd(), how, req); - RELEASE_ASSERT(res == IoUringResult::Ok, "unable to prepare cancel"); + RELEASE_ASSERT(res == IoUringResult::Ok, "unable to prepare shutdown"); } submit(); return req; @@ -294,10 +344,22 @@ void IoUringWorkerImpl::onFileEvent() { break; } - delete req; + // A `multishot` request is reused by the kernel across completions, so keep it alive until the + // kernel signals it will deliver no more completions for it. + if (!req->moreCompletions()) { + delete req; + } }); delay_submit_ = false; submit(); + + // forEveryCompletion reaps a bounded batch, so a burst that exceeds it (for example many + // `multishot` completions) leaves entries in the completion queue. The eventfd is edge-triggered + // and already drained, so re-arm the read event to reap the rest on the next loop iteration + // instead of stranding it until the next completion arrives. + if (io_uring_->hasReadyCompletions()) { + file_event_->activate(Event::FileReadyType::Read); + } } void IoUringWorkerImpl::submit() { @@ -308,15 +370,22 @@ void IoUringWorkerImpl::submit() { IoUringServerSocket::IoUringServerSocket(os_fd_t fd, IoUringWorkerImpl& parent, Event::FileReadyCb cb, uint32_t write_timeout_ms, + uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, bool enable_close_event) : IoUringSocketEntry(fd, parent, std::move(cb), enable_close_event), - write_timeout_ms_(write_timeout_ms) {} + write_timeout_ms_(write_timeout_ms), write_high_watermark_bytes_(write_high_watermark_bytes), + write_low_watermark_bytes_(write_low_watermark_bytes) {} IoUringServerSocket::IoUringServerSocket(os_fd_t fd, Buffer::Instance& read_buf, IoUringWorkerImpl& parent, Event::FileReadyCb cb, - uint32_t write_timeout_ms, bool enable_close_event) + uint32_t write_timeout_ms, + uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, + bool enable_close_event) : IoUringSocketEntry(fd, parent, std::move(cb), enable_close_event), - write_timeout_ms_(write_timeout_ms) { + write_timeout_ms_(write_timeout_ms), write_high_watermark_bytes_(write_high_watermark_bytes), + write_low_watermark_bytes_(write_low_watermark_bytes) { read_buf_.move(read_buf); } @@ -378,11 +447,18 @@ void IoUringServerSocket::write(Buffer::Instance& data) { ENVOY_LOG(trace, "write, buffer size = {}, fd = {}", data.length(), fd_); ASSERT(!shutdown_.has_value()); + // Apply backpressure by keeping the data in the handler when the write buffer is above the high + // watermark. The handler retries after a write event is delivered once the buffer drains. + if (above_write_high_watermark_) { + return; + } + // We need to reset the drain trackers, since the write and close is async in // the iouring. When the write is actually finished the above layer may already // release the drain trackers. write_buf_.move(data, data.length(), true); + checkWriteWatermarks(); submitWriteOrShutdownRequest(); } @@ -390,12 +466,17 @@ uint64_t IoUringServerSocket::write(const Buffer::RawSlice* slices, uint64_t num ENVOY_LOG(trace, "write, num_slices = {}, fd = {}", num_slice, fd_); ASSERT(!shutdown_.has_value()); + if (above_write_high_watermark_) { + return 0; + } + uint64_t bytes_written = 0; for (uint64_t i = 0; i < num_slice; i++) { write_buf_.add(slices[i].mem_, slices[i].len_); bytes_written += slices[i].len_; } + checkWriteWatermarks(); submitWriteOrShutdownRequest(); return bytes_written; } @@ -429,6 +510,24 @@ void IoUringServerSocket::onCancel(Request* req, int32_t result, bool injected) } void IoUringServerSocket::moveReadDataToBuffer(Request* req, size_t data_length) { + // A `multishot` read delivers data in a provided buffer. Wrap it as a fragment that releases the + // buffer to the pool once drained. The captured pool keeps the buffer memory alive even after the + // io_uring is gone, at which point releasing a buffer is a safe no-op. + if (read_is_multishot_) { + const int32_t buffer_id = req->bufferId(); + if (!multishotBufferIdValid(buffer_id)) { + return; + } + const IoUringBufferPoolSharedPtr& pool = parent_.bufferPool(); + Buffer::BufferFragment* fragment = new Buffer::BufferFragmentImpl( + pool->getBuffer(buffer_id), data_length, + [pool](const void* data, size_t, const Buffer::BufferFragmentImpl* this_fragment) { + pool->releaseBuffer(data); + delete this_fragment; + }); + read_buf_.addBufferFragment(*fragment); + return; + } ReadRequest* read_req = static_cast(req); Buffer::BufferFragment* fragment = new Buffer::BufferFragmentImpl( read_req->buf_.release(), data_length, @@ -444,7 +543,7 @@ void IoUringServerSocket::onReadCompleted(int32_t result) { ReadParam param{read_buf_, result}; read_param_ = param; IoUringSocketEntry::onReadCompleted(); - read_param_ = absl::nullopt; + read_param_ = std::nullopt; ENVOY_LOG(trace, "after read from socket, fd = {}, remain = {}", fd_, read_buf_.length()); } @@ -457,14 +556,31 @@ void IoUringServerSocket::onRead(Request* req, int32_t result, bool injected) { "onRead with result {}, fd = {}, injected = {}, status_ = {}, enable_close_event = {}", result, fd_, injected, static_cast(status_), enable_close_event_); if (!injected) { - read_req_ = nullptr; + // A `multishot` read stays armed across completions, so only release the request once the + // kernel signals it will deliver no more completions for it. + if (!read_is_multishot_ || !req->moreCompletions()) { + read_req_ = nullptr; + } // If the socket is going to close, discard all results. if (status_ == Closed && write_or_shutdown_req_ == nullptr && read_cancel_req_ == nullptr && write_or_shutdown_cancel_req_ == nullptr) { - if (result > 0 && keep_fd_open_) { - moveReadDataToBuffer(req, result); + if (result > 0) { + if (keep_fd_open_) { + moveReadDataToBuffer(req, result); + } else if (read_is_multishot_) { + // The data is dropped while closing, but the provided buffer must still be released to + // the ring so the pool is not depleted. + const int32_t buffer_id = req->bufferId(); + if (multishotBufferIdValid(buffer_id)) { + const IoUringBufferPoolSharedPtr& pool = parent_.bufferPool(); + pool->releaseBuffer(pool->getBuffer(buffer_id)); + } + } + } + // Wait for an armed `multishot` read to finish before closing. + if (read_req_ == nullptr) { + closeInternal(); } - closeInternal(); return; } } @@ -472,10 +588,28 @@ void IoUringServerSocket::onRead(Request* req, int32_t result, bool injected) { // Move read data from request to buffer or store the error. if (result > 0) { moveReadDataToBuffer(req, result); - } else { - if (result != -ECANCELED) { - read_error_ = result; + // Adaptive read sizing for the readv path. Grow the next read when the buffer is filled and + // reset it to the base size otherwise, so large transfers use fewer reads while small responses + // stay small. `Multishot` reads use fixed-size provided buffers and skip this. + if (!injected && !read_is_multishot_) { + const uint32_t base_read_size = parent_.readBufferSize(); + if (static_cast(result) >= next_read_size_) { + next_read_size_ = + std::min(next_read_size_ * 2, base_read_size * MaxReadBufferSizeMultiplier); + } else { + next_read_size_ = base_read_size; + } } + } else if (result == -ENOBUFS) { + // The provided buffer pool is exhausted, which ends the `multishot` read. Fall back to a readv + // for the next read so progress continues until buffers are recycled. + multishot_fallback_ = true; + } else if (read_is_multishot_ && (result == -EINVAL || result == -EOPNOTSUPP)) { + // The kernel registered the provided buffer ring but does not support `multishot` recv, which + // needs Linux 6.0. Disable `multishot` for this socket and use readv from now on. + multishot_disabled_ = true; + } else if (result != -ECANCELED) { + read_error_ = result; } // Discard calling back since the socket is not ready or closed. @@ -553,7 +687,7 @@ void IoUringServerSocket::onWriteCompleted(int32_t result) { WriteParam param{result}; write_param_ = param; IoUringSocketEntry::onWriteCompleted(); - write_param_ = absl::nullopt; + write_param_ = std::nullopt; } void IoUringServerSocket::onWrite(Request* req, int32_t result, bool injected) { @@ -580,9 +714,13 @@ void IoUringServerSocket::onWrite(Request* req, int32_t result, bool injected) { if (result > 0) { write_buf_.drain(result); ENVOY_LOG(trace, "drain write buf, drain size = {}, fd = {}", result, fd_); + checkWriteWatermarks(); } else { // Drain all write buf since the write failed. write_buf_.drain(write_buf_.length()); + // The write buffer is empty now, so clear backpressure to avoid a stuck write if the socket + // lingers before close. + above_write_high_watermark_ = false; if (!shutdown_.has_value() && status_ != Closed) { status_ = RemoteClosed; if (result == -EPIPE) { @@ -610,7 +748,18 @@ void IoUringServerSocket::onShutdown(Request* req, int32_t result, bool injected void IoUringServerSocket::closeInternal() { if (keep_fd_open_) { if (on_closed_cb_) { - on_closed_cb_(read_buf_); + // The fd is handed to another worker thread. `Multishot` reads leave provided buffer + // fragments in the read buffer that are bound to this worker's ring, so copy the data into + // owned memory and drain the read buffer here so those buffers return to the ring on this + // thread right away. + if (parent_.isMultishotEnabled() && read_buf_.length() > 0) { + Buffer::OwnedImpl owned_data; + owned_data.add(read_buf_); + read_buf_.drain(read_buf_.length()); + on_closed_cb_(owned_data); + } else { + on_closed_cb_(read_buf_); + } } cleanup(); return; @@ -621,9 +770,23 @@ void IoUringServerSocket::closeInternal() { } void IoUringServerSocket::submitReadRequest() { - if (!read_req_) { - read_req_ = parent_.submitReadRequest(*this); + if (read_req_) { + return; + } + // Prefer a `multishot` read when available so the kernel keeps delivering data without a new + // submission per read. After a buffer pool exhaustion fall back to a single readv, then re-arm + // `multishot` on the following read. + if (parent_.isMultishotEnabled() && !multishot_fallback_ && !multishot_disabled_) { + read_is_multishot_ = true; + read_req_ = parent_.submitReadMultishotRequest(*this); + return; } + multishot_fallback_ = false; + read_is_multishot_ = false; + if (next_read_size_ == 0) { + next_read_size_ = parent_.readBufferSize(); + } + read_req_ = parent_.submitReadRequest(*this, next_read_size_); } void IoUringServerSocket::submitWriteOrShutdownRequest() { @@ -642,10 +805,29 @@ void IoUringServerSocket::submitWriteOrShutdownRequest() { } } +void IoUringServerSocket::checkWriteWatermarks() { + if (!above_write_high_watermark_ && write_buf_.length() > write_high_watermark_bytes_) { + above_write_high_watermark_ = true; + ENVOY_LOG(trace, "write buffer above high watermark, fd = {}, size = {}", fd_, + write_buf_.length()); + } else if (above_write_high_watermark_ && write_buf_.length() <= write_low_watermark_bytes_) { + above_write_high_watermark_ = false; + ENVOY_LOG(trace, "write buffer below low watermark, fd = {}, size = {}", fd_, + write_buf_.length()); + // Inject a write event so the handler resumes writing the data it kept while `backpressured`. + if (!shutdown_.has_value() && status_ != Closed) { + injectCompletion(Request::RequestType::Write); + } + } +} + IoUringClientSocket::IoUringClientSocket(os_fd_t fd, IoUringWorkerImpl& parent, Event::FileReadyCb cb, uint32_t write_timeout_ms, + uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, bool enable_close_event) - : IoUringServerSocket(fd, parent, cb, write_timeout_ms, enable_close_event) {} + : IoUringServerSocket(fd, parent, cb, write_timeout_ms, write_high_watermark_bytes, + write_low_watermark_bytes, enable_close_event) {} void IoUringClientSocket::connect(const Network::Address::InstanceConstSharedPtr& address) { // Reuse read request since there is no read on connecting and connect is cancellable. diff --git a/source/common/io/io_uring_worker_impl.h b/source/common/io/io_uring_worker_impl.h index b2ebd1d380497..e329ac6767609 100644 --- a/source/common/io/io_uring_worker_impl.h +++ b/source/common/io/io_uring_worker_impl.h @@ -7,6 +7,8 @@ #include "source/common/common/logger.h" #include "source/common/io/io_uring_impl.h" +#include "absl/container/inlined_vector.h" + namespace Envoy { namespace Io { @@ -15,14 +17,16 @@ class ReadRequest : public Request { ReadRequest(IoUringSocket& socket, uint32_t size); std::unique_ptr buf_; - std::unique_ptr iov_; + struct iovec iov_; }; class WriteRequest : public Request { public: WriteRequest(IoUringSocket& socket, const Buffer::RawSliceVector& slices); - std::unique_ptr iov_; + // Inline storage matches the RawSliceVector capacity, so a typical write keeps its iovecs inside + // this request and avoids a second heap allocation per write. + absl::InlinedVector iov_; }; class IoUringSocketEntry; @@ -31,9 +35,11 @@ using IoUringSocketEntryPtr = std::unique_ptr; class IoUringWorkerImpl : public IoUringWorker, private Logger::Loggable { public: IoUringWorkerImpl(uint32_t io_uring_size, bool use_submission_queue_polling, - uint32_t read_buffer_size, uint32_t write_timeout_ms, - Event::Dispatcher& dispatcher); + bool enable_multishot_receive, uint32_t read_buffer_size, + uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, Event::Dispatcher& dispatcher); IoUringWorkerImpl(IoUringPtr&& io_uring, uint32_t read_buffer_size, uint32_t write_timeout_ms, + uint32_t write_high_watermark_bytes, uint32_t write_low_watermark_bytes, Event::Dispatcher& dispatcher); ~IoUringWorkerImpl() override; @@ -48,6 +54,10 @@ class IoUringWorkerImpl : public IoUringWorker, private Logger::Loggable read_error_; + std::optional read_error_; - // TODO (soulxu): We need water mark for write buffer. - // The upper layer will think the buffer released when the data copy into this write buffer. - // This leads to the `IntegrationTest.TestFloodUpstreamErrors` timeout, since the http layer - // always think the response is write successful, so flood protection is never kicked. - // // For write. iouring socket will write sequentially in the order of write_buf_ and shutdown_ // Unless the write_buf_ is empty, the shutdown operation will not be performed. + // Once write_buf_ grows above the high watermark the handler is told to stop writing, and it is + // notified to resume after write_buf_ drains below the low watermark. This applies backpressure + // to the upper layer so flood protection can kick in. Buffer::OwnedImpl write_buf_; - // shutdown_ has 3 states. A absl::nullopt indicates the socket has not been shutdown, a false + // shutdown_ has 3 states. A std::nullopt indicates the socket has not been shutdown, a false // value represents the socket wants to be shutdown but the shutdown has not been performed or // completed, and a true value means the socket has been shutdown. - absl::optional shutdown_{}; + std::optional shutdown_; // If there is in progress write_or_shutdown_req_ during closing, a write timeout timer may be // setup to cancel the write_or_shutdown_req_, either a write request or a shutdown request. So // we can make sure all SQEs bounding to the iouring socket is completed and the socket can be @@ -242,6 +284,8 @@ class IoUringServerSocket : public IoUringSocketEntry { Request* write_or_shutdown_cancel_req_{nullptr}; // This is used for tracking the close request. Request* close_req_{nullptr}; + // Whether the write buffer is above the high watermark and backpressure is being applied. + bool above_write_high_watermark_{false}; void closeInternal(); void submitReadRequest(); @@ -249,12 +293,14 @@ class IoUringServerSocket : public IoUringSocketEntry { void moveReadDataToBuffer(Request* req, size_t data_length); void onReadCompleted(int32_t result); void onWriteCompleted(int32_t result); + void checkWriteWatermarks(); }; class IoUringClientSocket : public IoUringServerSocket { public: IoUringClientSocket(os_fd_t fd, IoUringWorkerImpl& parent, Event::FileReadyCb cb, - uint32_t write_timeout_ms, bool enable_close_event); + uint32_t write_timeout_ms, uint32_t write_high_watermark_bytes, + uint32_t write_low_watermark_bytes, bool enable_close_event); void connect(const Network::Address::InstanceConstSharedPtr& address) override; void onConnect(Request* req, int32_t result, bool injected) override; diff --git a/source/common/json/json_internal.cc b/source/common/json/json_internal.cc index 91362cec77323..715ff72c02bdd 100644 --- a/source/common/json/json_internal.cc +++ b/source/common/json/json_internal.cc @@ -14,6 +14,7 @@ #include "source/common/common/hash.h" #include "source/common/common/utility.h" #include "source/common/protobuf/utility.h" +#include "source/common/runtime/runtime_features.h" // Do not let nlohmann/json leak outside of this file. #include "absl/strings/match.h" @@ -179,7 +180,7 @@ class Field : public Object { */ class ObjectHandler : public nlohmann::json_sax { public: - ObjectHandler() = default; + ObjectHandler(uint64_t max_depth) : max_depth_(max_depth) {} bool start_object(std::size_t) override; bool end_object() override; @@ -269,6 +270,7 @@ class ObjectHandler : public nlohmann::json_sax { std::string error_; std::string error_position_; + uint64_t max_depth_; static constexpr absl::string_view ErrorPrefix = "[json.exception."; }; @@ -613,6 +615,12 @@ void Field::validateSchema(const std::string&) const { } bool ObjectHandler::start_object(std::size_t) { + if (stack_.size() >= max_depth_) { + error_ = fmt::format("JSON nesting depth exceeds limit of {}", max_depth_); + error_position_ = absl::StrCat("line: ", line_number_); + return false; + } + FieldSharedPtr object = Field::createObject(); object->setLineNumberStart(line_number_); @@ -667,6 +675,12 @@ bool ObjectHandler::key(std::string& val) { } bool ObjectHandler::start_array(std::size_t) { + if (stack_.size() >= max_depth_) { + error_ = fmt::format("JSON nesting depth exceeds limit of {}", max_depth_); + error_position_ = absl::StrCat("line: ", line_number_); + return false; + } + FieldSharedPtr array = Field::createArray(); array->setLineNumberStart(line_number_); @@ -729,7 +743,13 @@ bool ObjectHandler::handleValueEvent(FieldSharedPtr ptr) { } // namespace absl::StatusOr Factory::loadFromString(const std::string& json) { - ObjectHandler handler; + // 10K JSON nesting depth is safe for the default Linux stack size + // Envoy default JSON nesting depth limit is set to 1K to reduce potential for DoS. + uint64_t max_depth = + Runtime::runtimeFeatureEnabled("envoy.reloadable_features.limit_json_parser_nesting_depth") + ? 1000 + : 10000; + ObjectHandler handler(max_depth); auto json_container = JsonContainer(json.c_str(), &handler); nlohmann::json::sax_parse(json_container, &handler); diff --git a/source/common/json/json_loader.cc b/source/common/json/json_loader.cc index 80f7ab45acef7..2bc60b2b3d9ef 100644 --- a/source/common/json/json_loader.cc +++ b/source/common/json/json_loader.cc @@ -1,7 +1,6 @@ #include "source/common/json/json_loader.h" #include "source/common/json/json_internal.h" -#include "source/common/runtime/runtime_features.h" namespace Envoy { namespace Json { diff --git a/source/common/json/json_rpc_field_extractor.cc b/source/common/json/json_rpc_field_extractor.cc index 484720fce9d6e..8b97a7b43bd79 100644 --- a/source/common/json/json_rpc_field_extractor.cc +++ b/source/common/json/json_rpc_field_extractor.cc @@ -459,7 +459,7 @@ void JsonRpcFieldExtractor::validateRequiredFields() { } void JsonRpcFieldExtractor::checkValidJsonRpc(absl::string_view name, - absl::optional value) { + std::optional value) { if (depth_ == 1) { if (name == jsonRpcField()) { if (value.has_value() && value.value() == jsonRpcVersion()) { diff --git a/source/common/json/json_rpc_field_extractor.h b/source/common/json/json_rpc_field_extractor.h index 399a9fcd1b65a..05ea067db6653 100644 --- a/source/common/json/json_rpc_field_extractor.h +++ b/source/common/json/json_rpc_field_extractor.h @@ -74,7 +74,7 @@ class JsonRpcFieldExtractor : public ProtobufUtil::converter::ObjectWriter, // Check if it is a valid JSON-RPC request or response. void checkValidJsonRpc(absl::string_view name, - absl::optional value = absl::nullopt); + std::optional value = std::nullopt); // Protocol-specific interface virtual bool isNotification(const std::string& method) const = 0; @@ -82,6 +82,7 @@ class JsonRpcFieldExtractor : public ProtobufUtil::converter::ObjectWriter, virtual absl::string_view jsonRpcVersion() const = 0; virtual absl::string_view jsonRpcField() const = 0; virtual absl::string_view methodField() const = 0; + // NOLINTNEXTLINE(readability-identifier-naming) virtual bool lists_supported() const = 0; Protobuf::Struct temp_storage_; // Store all fields temporarily @@ -92,7 +93,8 @@ class JsonRpcFieldExtractor : public ProtobufUtil::converter::ObjectWriter, struct NestedContext { Protobuf::Struct* struct_ptr{nullptr}; Protobuf::ListValue* list_ptr{nullptr}; - std::string field_name{}; + std::string field_name; + // NOLINTNEXTLINE(readability-identifier-naming) bool is_list() const { return list_ptr != nullptr; } }; std::stack context_stack_; diff --git a/source/common/json/wuffs_json/BUILD b/source/common/json/wuffs_json/BUILD new file mode 100644 index 0000000000000..3003e5d7811da --- /dev/null +++ b/source/common/json/wuffs_json/BUILD @@ -0,0 +1,35 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +# wuffs_impl.c is a pure C translation unit (the Wuffs amalgamation). +# It must NOT use envoy_cc_library because envoy_copts() adds C++-only flags +# (-Woverloaded-virtual, -Wold-style-cast) that GCC rejects for C source files. +cc_library( + name = "wuffs_impl", + srcs = ["wuffs_impl.c"], + copts = ["-Wno-unused-function"], + linkstatic = True, + deps = ["@wuffs"], +) + +envoy_cc_library( + name = "wuffs_json_cursor_lib", + srcs = ["wuffs_json_cursor.cc"], + hdrs = ["wuffs_json_cursor.h"], + deps = [ + ":wuffs_impl", + "@abseil-cpp//absl/base:nullability", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@wuffs", + ], +) diff --git a/source/common/json/wuffs_json/wuffs_impl.c b/source/common/json/wuffs_json/wuffs_impl.c new file mode 100644 index 0000000000000..03536945f2b81 --- /dev/null +++ b/source/common/json/wuffs_json/wuffs_impl.c @@ -0,0 +1,7 @@ +// This file is the single compilation unit that provides the Wuffs library +// implementation. Every other file that uses Wuffs includes wuffs-v0.4.c +// without WUFFS_IMPLEMENTATION (declarations only). +// +// WUFFS_IMPLEMENTATION must be defined in exactly one translation unit. +#define WUFFS_IMPLEMENTATION +#include "release/c/wuffs-v0.4.c" diff --git a/source/common/json/wuffs_json/wuffs_json_cursor.cc b/source/common/json/wuffs_json/wuffs_json_cursor.cc new file mode 100644 index 0000000000000..9d46fd0273b57 --- /dev/null +++ b/source/common/json/wuffs_json/wuffs_json_cursor.cc @@ -0,0 +1,450 @@ +#include "source/common/json/wuffs_json/wuffs_json_cursor.h" + +#include + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Json { +namespace Wuffs { + +namespace { + +// Append the content of one Wuffs STRING token to `out`. +// JSON STRING tokens carry either plain bytes (COPY) or the opening/closing +// quote delimiter (DROP). Backslash escapes do NOT arrive as STRING tokens — +// Wuffs emits them as UNICODE_CODE_POINT tokens handled separately below. +void appendStringToken(std::string& out, absl::string_view raw, uint64_t token_detail) { + if (token_detail & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_0_DST_1_SRC_DROP) { + return; + } + if (token_detail & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) { + out.append(raw); + } +} + +// UTF-8 encode a Unicode code point into buf (must be at least 4 bytes). +// Returns the number of bytes written (1–4). +size_t encodeCodePoint(uint8_t* buf, uint32_t code_point) { + if (code_point < 0x80u) { + buf[0] = static_cast(code_point); + return 1; + } else if (code_point < 0x800u) { + buf[0] = static_cast(0xC0u | (code_point >> 6u)); + buf[1] = static_cast(0x80u | (code_point & 0x3Fu)); + return 2; + } else if (code_point < 0x10000u) { + buf[0] = static_cast(0xE0u | (code_point >> 12u)); + buf[1] = static_cast(0x80u | ((code_point >> 6u) & 0x3Fu)); + buf[2] = static_cast(0x80u | (code_point & 0x3Fu)); + return 3; + } else { + buf[0] = static_cast(0xF0u | (code_point >> 18u)); + buf[1] = static_cast(0x80u | ((code_point >> 12u) & 0x3Fu)); + buf[2] = static_cast(0x80u | ((code_point >> 6u) & 0x3Fu)); + buf[3] = static_cast(0x80u | (code_point & 0x3Fu)); + return 4; + } +} + +void appendCodePoint(std::string& out, uint32_t code_point) { + uint8_t buf[4]; + out.append(reinterpret_cast(buf), encodeCodePoint(buf, code_point)); +} + +} // namespace + +WuffsJsonCursor::WuffsJsonCursor(Handler& handler, bool track_paths) + : handler_(handler), track_paths_(track_paths) { + decoder_ = wuffs_json__decoder::alloc(); + token_buf_ = + wuffs_base__slice_token__writer(wuffs_base__make_slice_token(token_data_, kTokenBufLen)); + // RFC 8259: JSON-text = ws value ws — trailing whitespace is valid. + decoder_->set_quirk(WUFFS_JSON__QUIRK_ALLOW_TRAILING_FILLER, 1); +} + +// Feeds one body chunk into the Wuffs JSON tokenizer and dispatches tokens +// to the Handler callbacks. +// +// Wuffs token model +// +// Each token has three fields: +// token_category — coarse kind (the switch below) +// token_detail — kind-specific bit flags +// token_len — source bytes consumed; body_src_pos_ is advanced by this +// for every token, giving a monotonic global byte counter. +// +// Token categories dispatched here: +// +// FILLER Whitespace, commas, colons, quote delimiters — skipped. +// +// STRUCTURE { } [ ] open/close. VBD__STRUCTURE__PUSH set on open; +// VBD__STRUCTURE__TO_DICT distinguishes { from [. +// +// STRING One segment of a string key or value (plain bytes only; +// escapes arrive as UNICODE_CODE_POINT). A logical string +// may span multiple tokens — `continued` is true on all +// but the last. Keys accumulate into key_buffer_; values +// route through openStringCapture / onStringChunk. +// VBD__STRING__CONVERT_0_DST_1_SRC_DROP — quote delimiter +// VBD__STRING__CONVERT_1_DST_1_SRC_COPY — plain content +// +// UNICODE_CODE_POINT Decoded backslash escape; token_detail holds the code +// point. Encoded to UTF-8 and forwarded to onStringChunk. +// +// NUMBER / LITERAL Raw bytes forwarded to onNumber; true/false/null +// dispatched to onBoolean / onNull. +// +// decode_tokens suspension: +// nullptr / note — document complete; set wuffs_done_. +// short_read — need more input; break and resume on next feed(). +// short_write — token_buf_ full; reset ring and retry. +// other — malformed JSON; return InvalidArgumentError. +absl::Status WuffsJsonCursor::feed(absl::string_view chunk, bool closed) { + if (!decoder_) { + return absl::InternalError("wuffs json: alloc failed"); + } + if (wuffs_done_) { + // TODO(tyxia) Revisit to see if it is right choice to return InvalidArgumentError. + return absl::InvalidArgumentError("wuffs json: feed() called after JSON document completed"); + } + + // If the previous call left unread bytes (a NUMBER or LITERAL that started + // near the end of the prior chunk), prepend them so Wuffs sees the full + // token in one contiguous buffer. STRING tokens are not affected: Wuffs + // flushes whatever string content it has before suspending, so no string + // bytes are ever left in pending_bytes_. + std::string pending_storage; + absl::string_view effective_chunk = chunk; + if (!pending_bytes_.empty()) { + // TODO(tyxia): copy only the minimal suffix of `chunk` needed to complete + // the token (scan for the first JSON terminator byte) rather than the + // entire chunk. Pending bytes are at most a few bytes, so the copy is + // cheap in practice, but stitching only the suffix would be cleaner. + pending_storage = std::move(pending_bytes_); + pending_storage.append(chunk.data(), chunk.size()); + effective_chunk = pending_storage; + } + pending_bytes_.clear(); + + // body_src_pos_ is a global byte counter across all feed() calls. Token + // offsets are expressed in the same global space, so (token_start - chunk_base) + // gives the offset into effective_chunk — needed for substr() when extracting + // raw bytes for STRING / NUMBER / LITERAL tokens. + const size_t chunk_base = body_src_pos_; + wuffs_base__io_buffer source_buf = wuffs_base__ptr_u8__reader( + const_cast(reinterpret_cast(effective_chunk.data())), + effective_chunk.size(), closed); + + while (true) { + wuffs_base__status status = wuffs_json__decoder__decode_tokens( + decoder_.get(), &token_buf_, &source_buf, wuffs_base__empty_slice_u8()); + + while (token_buf_.meta.ri < token_buf_.meta.wi) { + const wuffs_base__token* tok = &token_buf_.data.ptr[token_buf_.meta.ri++]; + const int64_t token_category = wuffs_base__token__value_base_category(tok); + const uint64_t token_detail = wuffs_base__token__value_base_detail(tok); + const uint64_t token_len = wuffs_base__token__length(tok); + const bool continued = wuffs_base__token__continued(tok); + const size_t token_start = body_src_pos_; + body_src_pos_ += token_len; + + switch (token_category) { + + case WUFFS_BASE__TOKEN__VBC__FILLER: + break; + + case WUFFS_BASE__TOKEN__VBC__STRUCTURE: { + if (auto s = handleStructureToken(token_detail, token_start); !s.ok()) { + return s; + } + break; + } + + case WUFFS_BASE__TOKEN__VBC__STRING: { + const absl::string_view raw = effective_chunk.substr(token_start - chunk_base, token_len); + if (auto s = handleStringToken(raw, token_detail, continued, token_start); !s.ok()) { + return s; + } + break; + } + + case WUFFS_BASE__TOKEN__VBC__UNICODE_CODE_POINT: { + if (auto s = handleUnicodeCodePointToken(token_detail); !s.ok()) { + return s; + } + break; + } + + case WUFFS_BASE__TOKEN__VBC__NUMBER: + case WUFFS_BASE__TOKEN__VBC__LITERAL: { + const absl::string_view raw = effective_chunk.substr(token_start - chunk_base, token_len); + if (auto s = handleNumberOrLiteralToken(token_category, raw, token_start); !s.ok()) { + return s; + } + break; + } + + default: + break; + } + } + + if (status.repr == nullptr || wuffs_base__status__is_note(&status)) { + wuffs_done_ = true; + if (closed && source_buf.meta.ri < effective_chunk.size()) { + return absl::InvalidArgumentError("wuffs json: trailing bytes after root value"); + } + break; + } + if (!wuffs_base__status__is_suspension(&status)) { + return absl::InvalidArgumentError( + absl::StrCat("wuffs json: ", wuffs_base__status__message(&status))); + } + if (status.repr == wuffs_base__suspension__short_read) { + // When Wuffs suspends mid-token it resets its read cursor to before the + // incomplete token, so source_buf.meta.ri points back to the token start. + // For NUMBER, Wuffs walks the cursor back byte-by-byte after reading digits since + // Wuffs need to read digits greedily, advancing iop_a_src until it sees a + // terminator character (whitespace, ,, }, ]) which means number is done. + // For LITERAL(null/true/false), Wuffs knows the exact length before reading so it checks the + // keyword without advancing the cursor at all, so no rewind is needed. + if (source_buf.meta.ri < effective_chunk.size()) { + // LITERAL is always 4–5 bytes (null/true/false) — completely bounded. + // For NUMBER split across chunks: cap pending_bytes_ to kMaxPendingBytes (see its + // declaration). Numbers that arrive complete with their terminator in the same chunk + // never reach this path and are bounded by the upstream max_body_bytes limit instead. + const size_t leftover = effective_chunk.size() - source_buf.meta.ri; + if (leftover > kMaxPendingBytes) { + return absl::InvalidArgumentError("wuffs json: number token too large"); + } + pending_bytes_.assign(effective_chunk.data() + source_buf.meta.ri, leftover); + } + break; + } + token_buf_.meta.ri = token_buf_.meta.wi = 0; // short_write: reset ring, retry + } + return absl::OkStatus(); +} + +absl::Status WuffsJsonCursor::handleStructureToken(uint64_t token_detail, size_t token_start) { + const bool is_push = (token_detail & WUFFS_BASE__TOKEN__VBD__STRUCTURE__PUSH) != 0; + const bool to_dict = (token_detail & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_DICT) != 0; + if (is_push) { + ++depth_; + if (depth_ > kMaxTrackedDepth - 1) { + return absl::InvalidArgumentError( + absl::StrCat("wuffs json: nesting depth exceeds ", kMaxTrackedDepth - 1)); + } + if (depth_ < kMaxTrackedDepth) { + seen_keys_[depth_].clear(); + is_dict_[depth_] = to_dict; + expecting_key_[depth_] = to_dict; + if (!to_dict) { + array_index_[depth_] = 0; + } + } + if (track_paths_ && depth_ <= kMaxTrackedDepth) { + push_key_[depth_] = (depth_ > 1 && is_dict_[depth_ - 1]) ? key_stack_[depth_ - 1] : ""; + } + // key for onContainerOpen is the parent dict key that triggered this container. + // Empty when the parent is an array or at root (depth 1). + const absl::string_view parent_key = (depth_ > 1 && is_dict_[depth_ - 1]) + ? absl::string_view(key_stack_[depth_ - 1]) + : absl::string_view(); + handler_.onContainerOpen(parent_key, to_dict, depth_, token_start); + } else { + const int pop_depth = depth_; + --depth_; + handler_.onContainerClose(pop_depth, body_src_pos_); + if (depth_ >= 1 && depth_ < kMaxTrackedDepth && is_dict_[depth_]) { + expecting_key_[depth_] = true; + } + if (depth_ >= 1 && depth_ < kMaxTrackedDepth && !is_dict_[depth_]) { + ++array_index_[depth_]; + } + } + return absl::OkStatus(); +} + +absl::Status WuffsJsonCursor::handleStringToken(absl::string_view raw, uint64_t token_detail, + bool continued, size_t token_start) { + if (!in_string_chain_) { + key_buffer_.clear(); + string_is_key_ = depth_ < kMaxTrackedDepth && is_dict_[depth_] && expecting_key_[depth_]; + if (string_is_key_) { + key_token_start_ = token_start; + } else { + // key_stack_[depth_] is always current here — onKey fired before this value. + const absl::string_view value_key = (depth_ < kMaxTrackedDepth && is_dict_[depth_]) + ? absl::string_view(key_stack_[depth_]) + : absl::string_view(); + string_capturing_ = handler_.openStringCapture(value_key, depth_, token_start); + string_chunk_active_ = string_capturing_; + } + } + if (string_is_key_) { + if ((token_detail & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) && + key_buffer_.size() + raw.size() > kMaxKeyBytes) { + return absl::InvalidArgumentError( + absl::StrCat("wuffs json: key exceeds ", kMaxKeyBytes, " bytes")); + } + if (!raw.empty()) { + appendStringToken(key_buffer_, raw, token_detail); + } + } else if (string_chunk_active_ && + (token_detail & WUFFS_BASE__TOKEN__VBD__STRING__CONVERT_1_DST_1_SRC_COPY) && + !raw.empty()) { + const absl::string_view value_key = (depth_ < kMaxTrackedDepth && is_dict_[depth_]) + ? absl::string_view(key_stack_[depth_]) + : absl::string_view(); + if (!handler_.onStringChunk(value_key, depth_, raw)) { + string_chunk_active_ = false; + } + } + in_string_chain_ = continued; + if (!in_string_chain_) { + if (string_is_key_) { + // TODO(tyxia): duplicate-key rejection is unconditional. There are 3 popular options + // here: reject / last-wins / first-wins + // Adding a configuration option here to enable different options. + if (depth_ < kMaxTrackedDepth && !seen_keys_[depth_].insert(key_buffer_).second) { + return absl::InvalidArgumentError( + absl::StrCat("wuffs json: duplicate key \"", key_buffer_, "\"")); + } + if (depth_ < kMaxTrackedDepth) { + key_stack_[depth_] = key_buffer_; + } + if (auto s = handler_.onKey(key_buffer_, depth_, key_token_start_); !s.ok()) { + return s; + } + if (depth_ < kMaxTrackedDepth) { + expecting_key_[depth_] = false; + } + } else { + const absl::string_view value_key = (depth_ < kMaxTrackedDepth && is_dict_[depth_]) + ? absl::string_view(key_stack_[depth_]) + : absl::string_view(); + handler_.closeStringCapture(value_key, depth_, body_src_pos_); + if (depth_ >= 1 && depth_ < kMaxTrackedDepth) { + if (is_dict_[depth_]) { + expecting_key_[depth_] = true; + } else { + ++array_index_[depth_]; + } + } + } + string_capturing_ = false; + string_chunk_active_ = false; + } + return absl::OkStatus(); +} + +// TODO(tyxia): Escape here to ensure any unicode token can be used, for example, for +// comparison routing, logging purposes. This requires re-escape in the re-encode phase. +// Investigate later to see if escape and re-escape are needed. +absl::Status WuffsJsonCursor::handleUnicodeCodePointToken(uint64_t token_detail) { + // Backslash escapes (\n, \t, …) arrive with VBD = decoded code point. + // in_string_chain_ is managed by surrounding STRING tokens, not updated here. + const uint32_t code_point = static_cast(token_detail); + const size_t utf8_len = (code_point < 0x80u) ? 1u + : (code_point < 0x800u) ? 2u + : (code_point < 0x10000u) ? 3u + : 4u; + if (string_is_key_) { + if (key_buffer_.size() + utf8_len > kMaxKeyBytes) { + return absl::InvalidArgumentError( + absl::StrCat("wuffs json: key exceeds ", kMaxKeyBytes, " bytes")); + } + appendCodePoint(key_buffer_, code_point); + } else if (string_chunk_active_) { + // TODO(tyxia): each UNICODE_CODE_POINT token triggers a separate onStringChunk + // call with 1-4 bytes. Consider buffering consecutive code points and flushing + // them as a single call on the next STRING/COPY token or chain end. + uint8_t buf[4]; + encodeCodePoint(buf, code_point); + const absl::string_view value_key = (depth_ < kMaxTrackedDepth && is_dict_[depth_]) + ? absl::string_view(key_stack_[depth_]) + : absl::string_view(); + if (!handler_.onStringChunk(value_key, depth_, + absl::string_view(reinterpret_cast(buf), utf8_len))) { + string_chunk_active_ = false; + } + } + return absl::OkStatus(); +} + +// NUMBER / LITERAL are single ring-buffer slot tokens; `continued` is always false. +// Chunk-boundary straddling uses short_read (coroutine suspends, resumes on next feed()). +absl::Status WuffsJsonCursor::handleNumberOrLiteralToken(int64_t token_category, + absl::string_view raw, + size_t token_start) { + const absl::string_view value_key = (depth_ < kMaxTrackedDepth && is_dict_[depth_]) + ? absl::string_view(key_stack_[depth_]) + : absl::string_view(); + if (token_category == WUFFS_BASE__TOKEN__VBC__NUMBER) { + if (auto s = handler_.onNumber(value_key, raw, depth_, token_start, body_src_pos_); !s.ok()) { + return s; + } + } else if (raw == "true" || raw == "false") { + if (auto s = handler_.onBoolean(value_key, raw[0] == 't', depth_, token_start, body_src_pos_); + !s.ok()) { + return s; + } + } else { + handler_.onNull(value_key, depth_, token_start, body_src_pos_); + } + if (depth_ >= 1 && depth_ < kMaxTrackedDepth) { + if (is_dict_[depth_]) { + expecting_key_[depth_] = true; + } else { + ++array_index_[depth_]; + } + } + return absl::OkStatus(); +} + +std::string WuffsJsonCursor::buildIndexedPath(int depth) const { + std::string path; + for (int d = 1; d <= depth && d < kMaxTrackedDepth; ++d) { + if (is_dict_[d]) { + // At the target depth, key_stack_[d] is the key currently being processed. + // At intermediate depths, the label is the key that opened the child container + // at d+1, stored in push_key_[d+1] at push time. + const std::string& label = (d == depth) ? key_stack_[d] : push_key_[d + 1]; + if (!path.empty()) { + path += '.'; + } + path += label; + } else { + // array_index_[d] is the count of elements completed so far at this depth, + // which equals the 0-based index of the element currently being processed. + path += '['; + path += std::to_string(array_index_[d]); + path += ']'; + } + } + return path; +} + +std::string WuffsJsonCursor::buildPatternPath(int depth) const { + std::string path; + for (int d = 1; d <= depth && d < kMaxTrackedDepth; ++d) { + if (is_dict_[d]) { + const std::string& label = (d == depth) ? key_stack_[d] : push_key_[d + 1]; + if (!path.empty()) { + path += '.'; + } + path += label; + } else { + path += "[]"; + } + } + return path; +} + +} // namespace Wuffs +} // namespace Json +} // namespace Envoy diff --git a/source/common/json/wuffs_json/wuffs_json_cursor.h b/source/common/json/wuffs_json/wuffs_json_cursor.h new file mode 100644 index 0000000000000..4241f64e9e189 --- /dev/null +++ b/source/common/json/wuffs_json/wuffs_json_cursor.h @@ -0,0 +1,331 @@ +#pragma once + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/status/status.h" +#include "absl/strings/string_view.h" + +// Wuffs JSON tokenizer — declarations only. +// WUFFS_IMPLEMENTATION is defined in exactly one translation unit: wuffs_impl.c. +#include "release/c/wuffs-v0.4.c" + +namespace Envoy { +namespace Json { +namespace Wuffs { + +// WuffsJsonCursor — streaming SAX-style JSON parser built on the Wuffs library. +// +// Tokenizes a JSON document delivered as a sequence of byte chunks and fires +// synchronous callbacks into a Handler. Chunks may split at any byte boundary. +// +// MyHandler h; +// WuffsJsonCursor cursor(h); +// for (const Chunk& c : body_chunks) { +// if (auto s = cursor.feed(c.data, c.is_last); !s.ok()) { /* error */ } +// } +// +// Depth and key +// +// Every callback receives `depth` (1 = root container) and, for dict values, +// `key` (the dict key to the left; "" for array elements). +// +// { "a": { "b": [ 1, 2 ] } } +// ^d=1 ^d=2 ^d=3 +// +// Callback sequence for {"messages": [{"role": "user"}]}: +// +// onContainerOpen (key="", is_dict=true, depth=1) +// onKey ("messages", depth=1) +// onContainerOpen (key="messages", is_dict=false, depth=2) +// onContainerOpen (key="", is_dict=true, depth=3) +// onKey ("role", depth=3) +// openStringCapture ("role", depth=3, token_start) → true/false +// onStringChunk ("role", depth=3, "user") +// closeStringCapture ("role", depth=3, token_end) +// onContainerClose (depth=3..1, ...) +// +// String capture +// +// Return true from openStringCapture to receive decoded UTF-8 via onStringChunk, +// false to discard at zero cost (no allocation, no further callbacks). +// onStringChunk returning false stops chunk delivery but parsing continues; +// closeStringCapture always fires with token_end. +// +// onStringChunk also enables fine-grained control mid-value: accumulate up to a +// limit and keep returning true, or return false to stop early — the cursor +// finishes parsing to the closing " either way. +// +// Container byte ranges +// +// onContainerOpen / onContainerClose deliver token_start / token_end, forming +// a half-open range [token_start, token_end) over the raw body bytes. +// +// Path tracking +// +// With track_paths=true, call from any callback: +// buildIndexedPath(depth) → "messages[0].role" +// buildPatternPath(depth) → "messages[].role" +// +class WuffsJsonCursor { +public: + // Handler — callback interface implemented by the JSON document consumer. + // + // All callbacks are invoked synchronously from within feed(). + // + // openStringCapture(key, depth, token_start) + // Called at the start of every non-key string value chain. + // Return true to receive decoded content via onStringChunk, or false to + // discard — no onStringChunk calls, no allocation, zero cost. + // `key` is the dict key for this value, or "" for array elements. + // `token_start` is the byte offset of the opening " in the body stream. + // + // onStringChunk(key, depth, chunk) + // Called for each decoded UTF-8 content chunk of a non-key string value. + // `chunk` is valid only for the duration of this call; do not retain it. + // Return true to keep receiving chunks, false to stop. If false, the + // cursor stops delivering chunks but continues parsing to find the closing + // "; closeStringCapture still fires. + // + // closeStringCapture(key, depth, token_end) + // Called when a non-key string chain completes (closing " seen). + // Always fires, even if openStringCapture returned false. + // `token_end` is the byte offset immediately past the closing ". + // + // onKey(key, depth) + // Called when a dict key completes. Return a non-OK Status to abort + // parsing (e.g. duplicate-key detection). + // + // onNumber(key, raw, depth) + // Called for JSON number literals (integer or floating-point). + // Return a non-OK Status to abort parsing. + // + // onBoolean(key, value, depth) + // Called for JSON true / false literals. + // Return a non-OK Status to abort parsing. + // + // onNull(key, depth) + // Called for JSON null literals. + // + // onContainerOpen(key, is_dict, depth, token_start) + // Called after depth has been incremented for a { or [ open. + // `key` is the parent dict key that opened this container, or "" if the + // parent is an array or this is the root container. + // `token_start` is the byte offset of the opening delimiter in the original + // body stream, useful for byte-range recording. + // + // onContainerClose(depth, token_end) + // Called with the container's depth before decrement and the byte offset + // immediately after the closing } or ]. + class Handler { + public: + virtual ~Handler() = default; + // Called once at the opening '"' of every non-key string value chain. + // This is the routing decision point: the handler inspects `key` and `depth` + // and decides whether to capture this value. + // + // Return true to receive content via onStringChunk. Return false to discard: + // no onStringChunk calls occur and no allocation is made — zero cost regardless + // of string size. closeStringCapture always fires either way. + // + // `key` — dict key immediately left of this value, or "" for array elements. + // `depth` — nesting depth of this string value. + // `token_start` — byte offset of the opening '"' in the body stream. + virtual bool openStringCapture(absl::string_view key, int depth, size_t token_start) = 0; + + // Called for each decoded UTF-8 content chunk of a non-key string value. + // Only called when openStringCapture returned true for this string. + // + // Chunks deliver fully decoded UTF-8: backslash escapes (e.g., \n, \t, etc.) + // are converted by the cursor before this call (e.g. \n -> 0x0A). + // `chunk` is valid only for the duration of this call — do not retain it. + // + // Return true to continue receiving chunks. Return false to stop: the cursor + // delivers no further chunks but continues parsing to find the closing '"'; + // closeStringCapture still fires with token_end. + virtual bool onStringChunk(absl::string_view key, int depth, absl::string_view chunk) = 0; + + // Called when a non-key string value chain completes (closing '"' seen). + // Always fires, even if openStringCapture returned false — giving every + // handler a [token_start, token_end) byte range for every string value + // without requiring content decoding. + // `token_end` is the byte offset immediately past the closing '"'. + virtual void closeStringCapture(absl::string_view key, int depth, size_t token_end) = 0; + + // Called when a dict key completes. + // `token_start` is the byte offset of the opening '"' of the key in the body + // stream. Combined with the token_end delivered by the subsequent value + // callback (closeStringCapture, onNumber, onBoolean, onNull, or + // onContainerClose), it gives the half-open byte range [token_start, token_end) + // covering the complete "key":value field — suitable for verbatim passthrough + // without DOM parsing. + // Return a non-OK Status to abort parsing (e.g. duplicate-key detection). + virtual absl::Status onKey(absl::string_view key, int depth, size_t token_start) = 0; + + // Called for JSON number literals (integer or floating-point). + // `token_start` / `token_end` delimit the scalar value token in the body stream. + // Together with the token_start from the preceding onKey call they cover the + // complete "key":value field byte range. + // Return a non-OK Status to abort parsing. + virtual absl::Status onNumber(absl::string_view key, absl::string_view raw, int depth, + size_t token_start, size_t token_end) = 0; + + // Called for JSON true / false literals. + // Return a non-OK Status to abort parsing. + virtual absl::Status onBoolean(absl::string_view key, bool value, int depth, size_t token_start, + size_t token_end) = 0; + + // Called for JSON null literals. + virtual void onNull(absl::string_view key, int depth, size_t token_start, size_t token_end) = 0; + + // Called after depth has been incremented for a { or [ open. + // `key` is the parent dict key that opened this container, or "" when + // the parent is an array or this is the root container. + // `token_start` is the byte offset of the opening { or [ in the body stream. + // + // PATH TRACKING NOTE — call buildPatternPath(depth-1), NOT buildPatternPath(depth): + // At the time this callback fires, key_stack_[depth] is not yet populated + // (no keys have been seen inside the new container). buildPatternPath(depth-1) + // gives the enclosing container's path, which combined with `key` identifies + // this container unambiguously. + // Example: onContainerOpen(key="parameters", is_dict=true, depth=5) + // buildPatternPath(4) → "tools[].function.parameters" ← correct + // buildPatternPath(5) → "tools[].function." ← wrong + // TODO(tyxia): add buildContainerPatternPath(key, is_dict, depth) + // convenience wrapper that hides this depth-1 subtlety. + virtual void onContainerOpen(absl::string_view key, bool is_dict, int depth, + size_t token_start) = 0; + + // Called with the container's depth before decrement and the byte offset + // immediately after the closing } or ]. + virtual void onContainerClose(int depth, size_t token_end) = 0; + }; + + explicit WuffsJsonCursor(Handler& handler, bool track_paths = false); + + WuffsJsonCursor(const WuffsJsonCursor&) = delete; + WuffsJsonCursor& operator=(const WuffsJsonCursor&) = delete; + WuffsJsonCursor(WuffsJsonCursor&&) = delete; + WuffsJsonCursor& operator=(WuffsJsonCursor&&) = delete; + + // Feed one body chunk. Set closed=true on the final chunk (signals EOF to Wuffs). + // Returns non-OK on malformed JSON or internal allocation failure. + absl::Status feed(absl::string_view chunk, bool closed); + + // Build dot-notation path strings for the field currently being selected. + // Return a dot-notation path string for the current cursor position. + // Must only be called from within a Handler callback while feed() is active. + // Requires track_paths=true at construction. + std::string buildIndexedPath(int depth) const; // e.g. "messages[0].role" + std::string buildPatternPath(int depth) const; // e.g. "messages[].role" + + // Monotonically increasing byte offset of the next source byte to be consumed. + // Matches the token_start / token_end values delivered to onContainerOpen / onContainerClose. + // TODO(tyxia): the outer filter should compare nextSourcePosition() against + // DecoderConfig::max_body_bytes between feed() calls and return ResourceExhausted + // before calling feed() again if the limit is exceeded. + size_t nextSourcePosition() const { return body_src_pos_; } + +private: + Handler& handler_; + const bool track_paths_; + + wuffs_json__decoder::unique_ptr decoder_; + static constexpr size_t kTokenBufLen = 256; + wuffs_base__token token_data_[kTokenBufLen]; + wuffs_base__token_buffer token_buf_{}; + + size_t body_src_pos_{0}; + bool wuffs_done_{false}; + + // Exclusive upper bound for per-depth state tracking: depths 1 through + // kMaxTrackedDepth-1 (currently 1–8) have full key/dup/path tracking. + // Value covers the deepest known OpenAI/Anthropic schema paths: + // tools[i].function.parameters.properties..type (depth 7) + // messages[i].content[j].content[k].text (depth 7) + // plus one buffer level for schemas with one extra level of nesting. + // + // Nesting beyond kMaxTrackedDepth-1 is rejected with InvalidArgumentError. + // Key/dup/path tracking accuracy is bounded by kMaxTrackedDepth-1 because + // the per-depth arrays below are stack-allocated at compile time. + // + // TODO(tyxia): replace the fixed arrays with std::vector to support + // dynamic depth so that max_depth_ can exceed kMaxTrackedDepth-1 without + // losing tracking accuracy. This removes the hard compile-time cap at the + // cost of per-push heap allocation; evaluate against the request-path perf + // budget before doing so. + static constexpr int kMaxTrackedDepth = 9; + // Cap key length at 256 bytes: well above any legitimate schema field name + // (longest observed ~25B, e.g. "input_audio_transcription") while bounding + // per-key allocation and guarding against DoS via unbounded key lengths. + static constexpr size_t kMaxKeyBytes = 256; + // kMaxPendingBytes is a hard limit against the byte-at-a-time DoS when a NUMBER token is split + // across chunk boundaries, NUMBER tokens that arrive complete with their terminator in one chunk + // bypass this path entirely — those are bounded by the max_body_bytes limit instead. A legitimate + // number is at most ~25 chars (64-bit int ≤ 20 digits; float with sign/decimal/exponent ≤ ~25). + // 64 bytes gives generous headroom. + static constexpr size_t kMaxPendingBytes = 64; + int depth_{0}; + bool is_dict_[kMaxTrackedDepth]{}; + bool expecting_key_[kMaxTrackedDepth]{}; + + // key_stack_[d] — most recently completed key at dict depth d. + // Always maintained (not gated on track_paths_) because it + // is forwarded as the `key` argument to Handler callbacks. + // push_key_[d] — key at depth d-1 that opened the container at depth d; + // captured at push time. Size kMaxTrackedDepth+1 so + // push_key_[kMaxTrackedDepth] is accessible when depth_ + // reaches kMaxTrackedDepth. + // Only maintained when track_paths_=true (used by + // buildIndexedPath/buildPatternPath). + // array_index_[d] — count of elements already completed at array depth d; + // reset to 0 on container open, incremented after each + // completed element (container close, scalar, or string value) + // when the enclosing container is an array. + // Used by buildIndexedPath (requires track_paths_=true). + std::string key_stack_[kMaxTrackedDepth]{}; + std::string push_key_[kMaxTrackedDepth + 1]{}; + int array_index_[kMaxTrackedDepth]{}; + // Tracks keys seen at each dict depth to detect and reject duplicates. + // Cleared on container open; flat_hash_set gives O(1) insert/lookup with + // one contiguous backing allocation (no per-node malloc unlike std::set). + absl::flat_hash_set seen_keys_[kMaxTrackedDepth]{}; + + bool in_string_chain_{false}; + + // Bytes unread by Wuffs before the last short_read suspension. Wuffs rewinds + // iop_a_src to before an incomplete NUMBER or LITERAL token before suspending, + // so those bytes must be prepended to the next chunk to form a contiguous + // buffer. Empty between feed() calls when no token straddles a boundary. + std::string pending_bytes_; + + // TODO(tyxia): Implement Handler : Handler that accepts DecoderConfig (max_body_bytes, + // max_inline_bytes, max_element_capture_bytes) and a list of ExtractFieldSpec; routes callbacks + // by matching buildPatternPath(depth) / buildPatternPath(depth-1) at onContainerOpen against + // specs and records element byte ranges. + // + // Implements the three-tier body-size logic (full capture semantic-only / reject). + // Implement a utility to parse ExtractFieldSpec path strings + // ("messages[].content[].text") into a normalized form directly comparable with + // buildPatternPath() output. At filter init, derive the max_depth constructor argument from the + // deepest ExtractFieldSpec path rather than the static default, tightening the DoS bound to + // exactly what the policy requires. + bool string_is_key_{false}; + bool string_capturing_{false}; // openStringCapture returned true for current value string + bool string_chunk_active_{false}; // onStringChunk hasn't returned false yet + std::string key_buffer_; + size_t key_token_start_{0}; + + absl::Status handleStructureToken(uint64_t token_detail, size_t token_start); + absl::Status handleStringToken(absl::string_view raw, uint64_t token_detail, bool continued, + size_t token_start); + absl::Status handleUnicodeCodePointToken(uint64_t token_detail); + absl::Status handleNumberOrLiteralToken(int64_t token_category, absl::string_view raw, + size_t token_start); +}; + +} // namespace Wuffs +} // namespace Json +} // namespace Envoy diff --git a/source/common/jwt/check_audience.h b/source/common/jwt/check_audience.h index 2b075425a4870..bd9e25c73a98d 100644 --- a/source/common/jwt/check_audience.h +++ b/source/common/jwt/check_audience.h @@ -39,7 +39,7 @@ class CheckAudience { std::set config_audiences_; }; -typedef std::unique_ptr CheckAudiencePtr; +using CheckAudiencePtr = std::unique_ptr; } // namespace JwtVerify } // namespace Envoy diff --git a/source/common/jwt/jwks.cc b/source/common/jwt/jwks.cc index 6bc3642a0400a..dcb0b6d429d8b 100644 --- a/source/common/jwt/jwks.cc +++ b/source/common/jwt/jwks.cc @@ -4,8 +4,7 @@ #include "source/common/jwt/jwks.h" -#include - +#include #include #include "source/common/jwt/struct_utils.h" @@ -133,7 +132,7 @@ class KeyGetter : public WithStatus { if (!absl::WebSafeBase64Unescape(s, &s_decoded)) { return nullptr; } - return bssl::UniquePtr(BN_bin2bn(castToUChar(s_decoded), s_decoded.length(), NULL)); + return bssl::UniquePtr(BN_bin2bn(castToUChar(s_decoded), s_decoded.length(), nullptr)); }; }; diff --git a/source/common/jwt/jwks.h b/source/common/jwt/jwks.h index 1769405dca1e4..d568253c1dc73 100644 --- a/source/common/jwt/jwks.h +++ b/source/common/jwt/jwks.h @@ -53,7 +53,7 @@ class Jwks : public WithStatus { bssl::UniquePtr bio_; bssl::UniquePtr x509_; }; - typedef std::unique_ptr PubkeyPtr; + using PubkeyPtr = std::unique_ptr; // Access to list of Jwks const std::vector& keys() const { return keys_; } @@ -68,7 +68,7 @@ class Jwks : public WithStatus { std::vector keys_; }; -typedef std::unique_ptr JwksPtr; +using JwksPtr = std::unique_ptr; } // namespace JwtVerify } // namespace Envoy diff --git a/source/common/jwt/jwt.h b/source/common/jwt/jwt.h index 1609f8352921a..fd8f01bc1f48f 100644 --- a/source/common/jwt/jwt.h +++ b/source/common/jwt/jwt.h @@ -62,7 +62,7 @@ struct Jwt { /** * Standard constructor. */ - Jwt() {} + Jwt() = default; /** * Copy constructor. The copy constructor is marked as explicit as the caller * should understand the copy operation is non-trivial as a complete diff --git a/source/common/jwt/simple_lru_cache_inl.h b/source/common/jwt/simple_lru_cache_inl.h index c74e6f73c3ed0..42facd57760fc 100644 --- a/source/common/jwt/simple_lru_cache_inl.h +++ b/source/common/jwt/simple_lru_cache_inl.h @@ -37,8 +37,6 @@ #pragma once -#include - #include #include #include @@ -106,14 +104,16 @@ template class SimpleLRUCacheConstIterator : public std::iterator> { public: - typedef typename MapType::const_iterator HashMapConstIterator; + using HashMapConstIterator = typename MapType::const_iterator; // Allow parent template's types to be referenced without qualification. - typedef typename SimpleLRUCacheConstIterator::reference reference; - typedef typename SimpleLRUCacheConstIterator::pointer pointer; + // NOLINTNEXTLINE(readability-identifier-naming) + using reference = typename SimpleLRUCacheConstIterator::reference; + // NOLINTNEXTLINE(readability-identifier-naming) + using pointer = typename SimpleLRUCacheConstIterator::pointer; // This default constructed Iterator can only be assigned to or destroyed. // All other operations give undefined behaviour. - SimpleLRUCacheConstIterator() {} + SimpleLRUCacheConstIterator() = default; SimpleLRUCacheConstIterator(HashMapConstIterator it, HashMapConstIterator end); SimpleLRUCacheConstIterator& operator++(); @@ -122,10 +122,12 @@ class SimpleLRUCacheConstIterator // For LRU mode, last_use_time() returns elements last use time. // See getLastUseTime() description for more information. + // NOLINTNEXTLINE(readability-identifier-naming) int64_t last_use_time() const { return last_use_; } // For age-based mode, insertion_time() returns elements insertion time. // See getInsertionTime() description for more information. + // NOLINTNEXTLINE(readability-identifier-naming) int64_t insertion_time() const { return last_use_; } friend bool operator==(const SimpleLRUCacheConstIterator& a, @@ -167,8 +169,9 @@ template struct SimpleLRUCacheElem { } void unlink() { - if (!isLinked()) + if (!isLinked()) { return; + } prev->next = next; next->prev = prev; prev = nullptr; @@ -190,17 +193,19 @@ template const int64_t SimpleLRUCacheElem @@ -228,7 +233,7 @@ template class SimpleLRUCacheB // NOTE: Be extremely careful when using ScopedLookup with Mutexes. This // code is safe since the lock will be released after the ScopedLookup is // destroyed. - // MutexLock l(&mu_); + // MutexLock l(mu_); // ScopedLookup lookup(....); // // This is NOT safe since the lock is released before the ScopedLookup is @@ -248,8 +253,9 @@ template class SimpleLRUCacheB value_(cache_->lookupWithOptions(key_, options_)) {} ~ScopedLookup() { - if (value_ != nullptr) + if (value_ != nullptr) { cache_->releaseWithOptions(key_, value_, options_); + } } const Key& key() const { return key_; } Value* value() const { return value_; } @@ -391,8 +397,9 @@ template class SimpleLRUCacheB // Remove all entries which have exceeded their max idle time or age // set using setMaxIdleSeconds() or setAgeBasedEviction() respectively. void removeExpiredEntries() { - if (max_idle_ >= 0) + if (max_idle_ >= 0) { discardIdle(max_idle_); + } } // Return current size of cache @@ -449,7 +456,8 @@ template class SimpleLRUCacheB } // STL style const_iterator support - typedef SimpleLRUCacheConstIterator const_iterator; + // NOLINTNEXTLINE(readability-identifier-naming) + using const_iterator = SimpleLRUCacheConstIterator; friend class SimpleLRUCacheConstIterator; const_iterator begin() const { return const_iterator(table_.begin(), table_.end()); } const_iterator end() const { return const_iterator(table_.end(), table_.end()); } @@ -490,13 +498,13 @@ template class SimpleLRUCacheB virtual bool isOverfull() const { return units_ > max_units_; } private: - typedef SimpleLRUCacheElem Elem; - typedef MapType Table; - typedef typename Table::iterator TableIterator; - typedef typename Table::const_iterator TableConstIterator; - typedef MapType DeferredTable; - typedef typename DeferredTable::iterator DeferredTableIterator; - typedef typename DeferredTable::const_iterator DeferredTableConstIterator; + using Elem = SimpleLRUCacheElem; + using Table = MapType; + using TableIterator = typename Table::iterator; + using TableConstIterator = typename Table::const_iterator; + using DeferredTable = MapType; + using DeferredTableIterator = typename DeferredTable::iterator; + using DeferredTableConstIterator = typename DeferredTable::const_iterator; Table table_; // Main table // Pinned entries awaiting to be released before they can be discarded. @@ -608,8 +616,9 @@ template void SimpleLRUCacheBase::removeUnpinned() { for (Elem* e = head_.next; e != &head_;) { Elem* next = e->next; - if (e->pin == 0) + if (e->pin == 0) { remove(e->key); + } e = next; } } @@ -650,8 +659,9 @@ Value* SimpleLRUCacheBase::lookupWithOptions( pinned_units_ += e->units; // We are pinning this entry, take it off the LRU list if we are in LRU // mode. In strict age-based mode entries stay on the list while pinned. - if (lru_ && options.update_eviction_order()) + if (lru_ && options.update_eviction_order()) { e->unlink(); + } } e->pin++; return e->value; @@ -707,8 +717,9 @@ void SimpleLRUCacheBase::releaseWithOptions( e->pin--; if (e->pin == 0) { - if (lru_ && options.update_eviction_order()) + if (lru_ && options.update_eviction_order()) { e->link(&head_); + } pinned_units_ -= e->units; if (isOverfullInternal()) { // This element is no longer needed, and we are full. So kick it out. @@ -735,8 +746,9 @@ void SimpleLRUCacheBase::insertPinned(const Key& k, Val // If we are in the strict age-based eviction mode, the entry goes on the LRU // list now and is never removed. In the LRU mode, the list will only contain // unpinned entries. - if (!lru_) + if (!lru_) { e->link(&head_); + } garbageCollect(); } @@ -793,8 +805,9 @@ bool SimpleLRUCacheBase::inDeferredTable(const Key& k, const Elem* const head = iter->second; const Elem* e = head; do { - if (e->value == value || value == nullptr) + if (e->value == value || value == nullptr) { return true; + } e = e->prev; } while (e != head); } @@ -858,8 +871,9 @@ static const int kAcceptableClockSynchronizationDriftCycles = 1; template void SimpleLRUCacheBase::discardIdle(int64_t max_idle) { - if (max_idle < 0) + if (max_idle < 0) { return; + } Elem* e = head_.prev; const int64_t threshold = SimpleCycleTimer::now() - max_idle; @@ -928,8 +942,9 @@ int64_t SimpleLRUCacheBase::deferredEntries() const { template int64_t SimpleLRUCacheBase::ageOfLRUItemInMicroseconds() const { - if (head_.prev == &head_) + if (head_.prev == &head_) { return 0; + } return kSecToUsec * (SimpleCycleTimer::now() - head_.prev->last_use_) / SimpleCycleTimer::frequency(); } @@ -939,8 +954,9 @@ int64_t SimpleLRUCacheBase::getLastUseTime(const Key& k // getLastUseTime works only in LRU mode assert(lru_); TableConstIterator iter = table_.find(k); - if (iter == table_.end()) + if (iter == table_.end()) { return -1; + } const Elem* e = iter->second; return e->last_use_; } @@ -950,8 +966,9 @@ int64_t SimpleLRUCacheBase::getInsertionTime(const Key& // getInsertionTime works only in age-based mode assert(!lru_); TableConstIterator iter = table_.find(k); - if (iter == table_.end()) + if (iter == table_.end()) { return -1; + } const Elem* e = iter->second; return e->last_use_; } @@ -1018,7 +1035,7 @@ class SimpleLRUCache total_units) {} protected: - virtual void removeElement(Value* value) { delete value; } + void removeElement(Value* value) override { delete value; } private: GOOGLE_DISALLOW_EVIL_CONSTRUCTORS(SimpleLRUCache); @@ -1028,8 +1045,8 @@ template class SimpleLRUCacheWithDeleter : public SimpleLRUCacheBase< Key, Value, absl::flat_hash_map*, H, EQ>, EQ> { - typedef absl::flat_hash_map*, H, EQ> HashMap; - typedef SimpleLRUCacheBase Base; + using HashMap = absl::flat_hash_map*, H, EQ>; + using Base = SimpleLRUCacheBase; public: explicit SimpleLRUCacheWithDeleter(int64_t total_units) : Base(total_units), deleter_() {} @@ -1038,7 +1055,7 @@ class SimpleLRUCacheWithDeleter : Base(total_units), deleter_(deleter) {} protected: - virtual void removeElement(Value* value) { deleter_(value); } + void removeElement(Value* value) override { deleter_(value); } private: Deleter deleter_; diff --git a/source/common/jwt/status.h b/source/common/jwt/status.h index ba5c7ff48009b..5c5d5fc9217d3 100644 --- a/source/common/jwt/status.h +++ b/source/common/jwt/status.h @@ -225,7 +225,7 @@ std::string getStatusString(Status status); */ class WithStatus { public: - WithStatus() : status_(Status::Ok) {} + WithStatus() = default; /** * Get the current status. @@ -245,7 +245,7 @@ class WithStatus { private: // The internal status. - Status status_; + Status status_{Status::Ok}; }; } // namespace JwtVerify diff --git a/source/common/jwt/struct_utils.h b/source/common/jwt/struct_utils.h index 116d9bf3985f2..2d92e8beaee3c 100644 --- a/source/common/jwt/struct_utils.h +++ b/source/common/jwt/struct_utils.h @@ -15,29 +15,37 @@ class StructUtils { public: StructUtils(const Protobuf::Struct& struct_pb); + // NOLINTBEGIN(readability-identifier-naming) enum FindResult { OK = 0, MISSING, WRONG_TYPE, OUT_OF_RANGE, }; + // NOLINTEND(readability-identifier-naming) + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetString(const std::string& name, std::string* str_value); // Return error if the JSON value is not within a positive 64 bit integer // range. The decimals in the JSON value are dropped. + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetUInt64(const std::string& name, uint64_t* int_value); + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetDouble(const std::string& name, double* double_value); + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetBoolean(const std::string& name, bool* bool_value); // Get string or list of string, designed to get "aud" field // "aud" can be either string array or string. // Try as string array, read it as empty array if doesn't exist. + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetStringList(const std::string& name, std::vector* list); // Find the value with nested names. + // NOLINTNEXTLINE(readability-identifier-naming) FindResult GetValue(const std::string& nested_names, const Protobuf::Value*& found); private: diff --git a/source/common/listener_manager/BUILD b/source/common/listener_manager/BUILD index e39899aaeb548..5bbff45474721 100644 --- a/source/common/listener_manager/BUILD +++ b/source/common/listener_manager/BUILD @@ -30,6 +30,7 @@ envoy_cc_library( "//envoy/config:typed_metadata_interface", "//envoy/network:connection_interface", "//envoy/network:listener_interface", + "//envoy/network:udp_packet_writer_factory_factory_interface", "//envoy/server:api_listener_interface", "//envoy/server:filter_config_interface", "//envoy/server:listener_manager_interface", @@ -38,7 +39,9 @@ envoy_cc_library( "//source/common/access_log:access_log_lib", "//source/common/api:os_sys_calls_lib", "//source/common/common:basic_resource_lib", + "//source/common/common:cpu_affinity_lib", "//source/common/common:empty_string", + "//source/common/common:thread_lib", "//source/common/config:utility_lib", "//source/common/http:conn_manager_lib", "//source/common/init:manager_lib", @@ -62,7 +65,9 @@ envoy_cc_library( "//source/server:factory_context_lib", "//source/server:listener_manager_factory_lib", "//source/server:transport_socket_config_lib", + "@abseil-cpp//absl/types:span", "@envoy_api//envoy/admin/v3:pkg_cc_proto", + "@envoy_api//envoy/config/bootstrap/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", "@envoy_api//envoy/config/metrics/v3:pkg_cc_proto", @@ -114,7 +119,10 @@ envoy_cc_library( "//source/server:configuration_lib", "//source/server:factory_context_lib", "@envoy_api//envoy/config/listener/v3:pkg_cc_proto", - ], + "@envoy_api//envoy/extensions/transport_sockets/raw_buffer/v3:pkg_cc_proto", + ] + envoy_select_enable_http3([ + "//source/common/quic:quic_server_transport_socket_factory_lib", + ]), ) envoy_cc_library( @@ -137,7 +145,7 @@ envoy_cc_library( "//envoy/server:listener_manager_interface", "//source/common/common:cleanup_lib", "//source/common/config:api_version_lib", - "//source/common/config:subscription_base_interface", + "//source/common/config:resource_type_helper_lib", "//source/common/config:utility_lib", "//source/common/grpc:common_lib", "//source/common/init:target_lib", diff --git a/source/common/listener_manager/active_stream_listener_base.cc b/source/common/listener_manager/active_stream_listener_base.cc index 2cae0d736ed13..b8ed93a388ded 100644 --- a/source/common/listener_manager/active_stream_listener_base.cc +++ b/source/common/listener_manager/active_stream_listener_base.cc @@ -152,6 +152,45 @@ ActiveConnections& OwnedActiveStreamListenerBase::getOrCreateActiveConnections( return *connections; } +void OwnedActiveStreamListenerBase::onFilterChainDrainStart( + const std::list& draining_filter_chains) { + // A drain callback may synchronously close the current connection, which removes it from + // `connections_` via removeConnection(). Capture `next` before invoking onDrain() so the + // std::list iteration survives erasure of the current node. Pin `is_deleting_` while + // iterating so removeConnection() does not also erase the map entry mid-loop if the + // filter chain's last connection closes. + const bool was_deleting = is_deleting_; + is_deleting_ = true; + for (const auto* filter_chain : draining_filter_chains) { + auto map_iter = connections_by_context_.find(filter_chain); + if (map_iter == connections_by_context_.end()) { + continue; + } + auto& connections = map_iter->second->connections_; + for (auto it = connections.begin(); it != connections.end();) { + auto next = std::next(it); + (*it)->connection_->onDrain(); + it = next; + } + } + is_deleting_ = was_deleting; +} + +void OwnedActiveStreamListenerBase::onListenerDrainStart() { + // See onFilterChainDrainStart for why `next` is captured and `is_deleting_` is pinned. + const bool was_deleting = is_deleting_; + is_deleting_ = true; + for (auto& entry : connections_by_context_) { + auto& connections = entry.second->connections_; + for (auto it = connections.begin(); it != connections.end();) { + auto next = std::next(it); + (*it)->connection_->onDrain(); + it = next; + } + } + is_deleting_ = was_deleting; +} + void OwnedActiveStreamListenerBase::removeFilterChain(const Network::FilterChain* filter_chain) { auto iter = connections_by_context_.find(filter_chain); if (iter == connections_by_context_.end()) { diff --git a/source/common/listener_manager/active_stream_listener_base.h b/source/common/listener_manager/active_stream_listener_base.h index 59146db3ccd4d..14418ec51004f 100644 --- a/source/common/listener_manager/active_stream_listener_base.h +++ b/source/common/listener_manager/active_stream_listener_base.h @@ -203,6 +203,11 @@ class OwnedActiveStreamListenerBase : public ActiveStreamListenerBase { */ void removeConnection(ActiveTcpConnection& connection); + // Network::ConnectionHandler::ActiveListener + void onFilterChainDrainStart( + const std::list& draining_filter_chains) override; + void onListenerDrainStart() override; + protected: /** * Return the active connections container attached to the given filter chain. diff --git a/source/common/listener_manager/active_tcp_listener.cc b/source/common/listener_manager/active_tcp_listener.cc index e5a7e82cd9d9c..2f8558b2da18d 100644 --- a/source/common/listener_manager/active_tcp_listener.cc +++ b/source/common/listener_manager/active_tcp_listener.cc @@ -109,9 +109,9 @@ void ActiveTcpListener::recordConnectionsAcceptedOnSocketEvent(uint32_t connecti void ActiveTcpListener::onAcceptWorker(Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections, bool rebalanced, - const absl::optional& network_namespace) { + const std::optional& network_namespace) { // Get Round Trip Time - absl::optional t = socket->lastRoundTripTime(); + std::optional t = socket->lastRoundTripTime(); if (t.has_value()) { socket->connectionInfoProvider().setRoundTripTime(t.value()); } diff --git a/source/common/listener_manager/active_tcp_listener.h b/source/common/listener_manager/active_tcp_listener.h index 299fc34d51267..d2af55fc79fdc 100644 --- a/source/common/listener_manager/active_tcp_listener.h +++ b/source/common/listener_manager/active_tcp_listener.h @@ -79,7 +79,7 @@ class ActiveTcpListener final : public Network::TcpListenerCallbacks, void post(Network::ConnectionSocketPtr&& socket) override; void onAcceptWorker(Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections, bool rebalanced, - const absl::optional& network_namespace) override; + const std::optional& network_namespace) override; void newActiveConnection(const Network::FilterChain& filter_chain, Network::ServerConnectionPtr server_conn_ptr, @@ -94,7 +94,7 @@ class ActiveTcpListener final : public Network::TcpListenerCallbacks, Network::TcpConnectionHandler& tcp_conn_handler_; // The number of connections currently active on this listener. This is typically used for // connection balancing across per-handler listeners. - std::atomic num_listener_connections_{}; + std::atomic num_listener_connections_{0}; Network::ConnectionBalancer& connection_balancer_; // This is the address this listener is listening on. It's used to get the correct listener @@ -103,6 +103,6 @@ class ActiveTcpListener final : public Network::TcpListenerCallbacks, Network::Address::InstanceConstSharedPtr listen_address_; }; -using ActiveTcpListenerOptRef = absl::optional>; +using ActiveTcpListenerOptRef = std::optional>; } // namespace Server } // namespace Envoy diff --git a/source/common/listener_manager/active_tcp_socket.cc b/source/common/listener_manager/active_tcp_socket.cc index a7a3bc2f868c9..935322b260b48 100644 --- a/source/common/listener_manager/active_tcp_socket.cc +++ b/source/common/listener_manager/active_tcp_socket.cc @@ -12,7 +12,7 @@ namespace Server { ActiveTcpSocket::ActiveTcpSocket(ActiveStreamListenerBase& listener, Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections, - const absl::optional& network_namespace) + const std::optional& network_namespace) : listener_(listener), socket_(std::move(socket)), hand_off_restored_destination_connections_(hand_off_restored_destination_connections), iter_(accept_filters_.end()), @@ -26,7 +26,6 @@ ActiveTcpSocket::ActiveTcpSocket(ActiveStreamListenerBase& listener, stream_info_->filterState()->setData( Network::DownstreamNetworkNamespace::key(), std::make_unique(*network_namespace), - StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::Connection); } @@ -221,7 +220,7 @@ void ActiveTcpSocket::newConnection() { // Note also that we must account for the number of connections properly across both listeners. // TODO(mattklein123): See note in ~ActiveTcpSocket() related to making this accounting better. listener_.decNumConnections(); - absl::optional network_namespace; + std::optional network_namespace; if (const auto* obj = stream_info_->filterState()->getDataReadOnlyGeneric( Network::DownstreamNetworkNamespace::key()); obj) { diff --git a/source/common/listener_manager/active_tcp_socket.h b/source/common/listener_manager/active_tcp_socket.h index a978ba11ea683..5c73f64a22453 100644 --- a/source/common/listener_manager/active_tcp_socket.h +++ b/source/common/listener_manager/active_tcp_socket.h @@ -33,7 +33,7 @@ class ActiveTcpSocket : public Network::ListenerFilterManager, public: ActiveTcpSocket(ActiveStreamListenerBase& listener, Network::ConnectionSocketPtr&& socket, bool hand_off_restored_destination_connections, - const absl::optional& network_namespace); + const std::optional& network_namespace); ~ActiveTcpSocket() override; void onTimeout(); diff --git a/source/common/listener_manager/connection_handler_impl.cc b/source/common/listener_manager/connection_handler_impl.cc index efeaf37fd71c1..156b54ecf8438 100644 --- a/source/common/listener_manager/connection_handler_impl.cc +++ b/source/common/listener_manager/connection_handler_impl.cc @@ -18,12 +18,12 @@ namespace Envoy { namespace Server { ConnectionHandlerImpl::ConnectionHandlerImpl(Event::Dispatcher& dispatcher, - absl::optional worker_index) + std::optional worker_index) : worker_index_(worker_index), dispatcher_(dispatcher), per_handler_stat_prefix_(dispatcher.name() + "."), disable_listeners_(false) {} ConnectionHandlerImpl::ConnectionHandlerImpl(Event::Dispatcher& dispatcher, - absl::optional worker_index, + std::optional worker_index, OverloadManager& overload_manager, OverloadManager& null_overload_manager) : worker_index_(worker_index), dispatcher_(dispatcher), overload_manager_(overload_manager), @@ -37,7 +37,7 @@ void ConnectionHandlerImpl::decNumConnections() { --num_handler_connections_; } -void ConnectionHandlerImpl::addListener(absl::optional overridden_listener, +void ConnectionHandlerImpl::addListener(std::optional overridden_listener, Network::ListenerConfig& config, Runtime::Loader& runtime, Random::RandomGenerator& random) { if (overridden_listener.has_value()) { @@ -85,9 +85,9 @@ void ConnectionHandlerImpl::addListener(absl::optional overridden_list config.shouldBypassOverloadManager() ? (null_overload_manager_ ? makeOptRef(null_overload_manager_->getThreadLocalOverloadState()) - : absl::nullopt) + : std::nullopt) : (overload_manager_ ? makeOptRef(overload_manager_->getThreadLocalOverloadState()) - : absl::nullopt); + : std::nullopt); for (auto& socket_factory : config.listenSocketFactories()) { auto address = socket_factory->localAddress(); // worker_index_ doesn't have a value on the main thread for the admin server. @@ -220,7 +220,7 @@ ConnectionHandlerImpl::findPerAddressActiveListenerDetails( } } - return absl::nullopt; + return std::nullopt; } Network::UdpListenerCallbacksOptRef @@ -233,7 +233,7 @@ ConnectionHandlerImpl::getUdpListenerCallbacks(uint64_t listener_tag, ASSERT(listener->get().udpListener().has_value()); return listener->get().udpListener(); } - return absl::nullopt; + return std::nullopt; } void ConnectionHandlerImpl::removeFilterChains( @@ -265,6 +265,27 @@ void ConnectionHandlerImpl::stopListeners(uint64_t listener_tag, } } +void ConnectionHandlerImpl::onFilterChainDrain( + uint64_t listener_tag, const std::list& filter_chains) { + if (auto listener_it = listener_map_by_tag_.find(listener_tag); + listener_it != listener_map_by_tag_.end()) { + listener_it->second->invokeListenerMethod( + [&filter_chains](Network::ConnectionHandler::ActiveListener& listener) { + listener.onFilterChainDrainStart(filter_chains); + }); + } +} + +void ConnectionHandlerImpl::onListenerDrain(uint64_t listener_tag) { + if (auto listener_it = listener_map_by_tag_.find(listener_tag); + listener_it != listener_map_by_tag_.end()) { + listener_it->second->invokeListenerMethod( + [](Network::ConnectionHandler::ActiveListener& listener) { + listener.onListenerDrainStart(); + }); + } +} + void ConnectionHandlerImpl::stopListeners() { for (auto& iter : listener_map_by_tag_) { iter.second->invokeListenerMethod([](Network::ConnectionHandler::ActiveListener& listener) { @@ -309,6 +330,18 @@ void ConnectionHandlerImpl::setListenerRejectFraction(UnitFloat reject_fraction) } } +void ConnectionHandlerImpl::closeIdleHttpConnections(bool is_saturated) { + for (const auto& [tag, listener] : listener_map_by_tag_) { + listener->invokeListenerMethod( + [is_saturated](Network::ConnectionHandler::ActiveListener& active_listener) { + if (active_listener.listener() != nullptr && + !active_listener.listener()->shouldBypassOverloadManager()) { + active_listener.onCloseIdleHttpConnections(is_saturated); + } + }); + } +} + Network::InternalListenerOptRef ConnectionHandlerImpl::findByAddress(const Network::Address::InstanceConstSharedPtr& address) { ASSERT(address->type() == Network::Address::Type::EnvoyInternal); @@ -323,19 +356,19 @@ ConnectionHandlerImpl::findByAddress(const Network::Address::InstanceConstShared ConnectionHandlerImpl::ActiveTcpListenerOptRef ConnectionHandlerImpl::PerAddressActiveListenerDetails::tcpListener() { auto* val = absl::get_if>(&typed_listener_); - return (val != nullptr) ? absl::make_optional(*val) : absl::nullopt; + return (val != nullptr) ? std::make_optional(*val) : std::nullopt; } ConnectionHandlerImpl::UdpListenerCallbacksOptRef ConnectionHandlerImpl::PerAddressActiveListenerDetails::udpListener() { auto* val = absl::get_if>(&typed_listener_); - return (val != nullptr) ? absl::make_optional(*val) : absl::nullopt; + return (val != nullptr) ? std::make_optional(*val) : std::nullopt; } Network::InternalListenerOptRef ConnectionHandlerImpl::PerAddressActiveListenerDetails::internalListener() { auto* val = absl::get_if>(&typed_listener_); - return (val != nullptr) ? makeOptRef(val->get()) : absl::nullopt; + return (val != nullptr) ? makeOptRef(val->get()) : std::nullopt; } ConnectionHandlerImpl::ActiveListenerDetailsOptRef @@ -343,7 +376,7 @@ ConnectionHandlerImpl::findActiveListenerByTag(uint64_t listener_tag) { if (auto iter = listener_map_by_tag_.find(listener_tag); iter != listener_map_by_tag_.end()) { return *iter->second; } - return absl::nullopt; + return std::nullopt; } Network::BalancedConnectionHandlerOptRef @@ -356,7 +389,7 @@ ConnectionHandlerImpl::getBalancedHandlerByTag(uint64_t listener_tag, ASSERT(active_listener->get().tcpListener().has_value()); return active_listener->get().tcpListener().value().get(); } - return absl::nullopt; + return std::nullopt; } Network::ListenerPtr ConnectionHandlerImpl::createListener( @@ -403,7 +436,7 @@ ConnectionHandlerImpl::getBalancedHandlerByAddress(const Network::Address::Insta details->typed_listener_)) .value() .get()) - : absl::nullopt; + : std::nullopt; } REGISTER_FACTORY(ConnectionHandlerFactoryImpl, ConnectionHandlerFactory); diff --git a/source/common/listener_manager/connection_handler_impl.h b/source/common/listener_manager/connection_handler_impl.h index c5a704ed9a70b..d69fc9cfcf3d5 100644 --- a/source/common/listener_manager/connection_handler_impl.h +++ b/source/common/listener_manager/connection_handler_impl.h @@ -33,18 +33,18 @@ class ConnectionHandlerImpl : public ConnectionHandler, Logger::Loggable { public: using UdpListenerCallbacksOptRef = - absl::optional>; - using ActiveTcpListenerOptRef = absl::optional>; + std::optional>; + using ActiveTcpListenerOptRef = std::optional>; - ConnectionHandlerImpl(Event::Dispatcher& dispatcher, absl::optional worker_index); - ConnectionHandlerImpl(Event::Dispatcher& dispatcher, absl::optional worker_index, + ConnectionHandlerImpl(Event::Dispatcher& dispatcher, std::optional worker_index); + ConnectionHandlerImpl(Event::Dispatcher& dispatcher, std::optional worker_index, OverloadManager& overload_manager, OverloadManager& null_overload_manager); // Network::ConnectionHandler uint64_t numConnections() const override { return num_handler_connections_; } void incNumConnections() override; void decNumConnections() override; - void addListener(absl::optional overridden_listener, Network::ListenerConfig& config, + void addListener(std::optional overridden_listener, Network::ListenerConfig& config, Runtime::Loader& runtime, Random::RandomGenerator& random) override; void removeListeners(uint64_t listener_tag) override; void removeFilterChains(uint64_t listener_tag, @@ -53,10 +53,14 @@ class ConnectionHandlerImpl : public ConnectionHandler, void stopListeners(uint64_t listener_tag, const Network::ExtraShutdownListenerOptions& options) override; void stopListeners() override; + void onFilterChainDrain(uint64_t listener_tag, + const std::list& filter_chains) override; + void onListenerDrain(uint64_t listener_tag) override; void disableListeners() override; void enableListeners() override; void setListenerRejectFraction(UnitFloat reject_fraction) override; const std::string& statPrefix() const override { return per_handler_stat_prefix_; } + void closeIdleHttpConnections(bool is_saturated) override; // Network::TcpConnectionHandler Event::Dispatcher& dispatcher() override { return dispatcher_; } @@ -141,23 +145,23 @@ class ConnectionHandlerImpl : public ConnectionHandler, } }; - using ActiveListenerDetailsOptRef = absl::optional>; + using ActiveListenerDetailsOptRef = std::optional>; ActiveListenerDetailsOptRef findActiveListenerByTag(uint64_t listener_tag); using PerAddressActiveListenerDetailsOptRef = - absl::optional>; + std::optional>; PerAddressActiveListenerDetailsOptRef findPerAddressActiveListenerDetails(const ActiveListenerDetailsOptRef active_listener_details, const Network::Address::Instance& address); // This has a value on worker threads, and no value on the main thread. - const absl::optional worker_index_; + const std::optional worker_index_; Event::Dispatcher& dispatcher_; OptRef overload_manager_; OptRef null_overload_manager_; const std::string per_handler_stat_prefix_; // Declare before its users ActiveListenerDetails. - std::atomic num_handler_connections_{}; + std::atomic num_handler_connections_{0}; absl::flat_hash_map> listener_map_by_tag_; absl::flat_hash_map> tcp_listener_map_by_address_; @@ -171,7 +175,7 @@ class ConnectionHandlerImpl : public ConnectionHandler, class ConnectionHandlerFactoryImpl : public ConnectionHandlerFactory { public: std::unique_ptr - createConnectionHandler(Event::Dispatcher& dispatcher, absl::optional worker_index, + createConnectionHandler(Event::Dispatcher& dispatcher, std::optional worker_index, OverloadManager& overload_manager, OverloadManager& null_overload_manager) override { return std::make_unique(dispatcher, worker_index, overload_manager, @@ -179,7 +183,7 @@ class ConnectionHandlerFactoryImpl : public ConnectionHandlerFactory { } std::unique_ptr createConnectionHandler(Event::Dispatcher& dispatcher, - absl::optional worker_index) override { + std::optional worker_index) override { return std::make_unique(dispatcher, worker_index); } diff --git a/source/common/listener_manager/filter_chain_manager_impl.cc b/source/common/listener_manager/filter_chain_manager_impl.cc index f36ee6513144f..39b0ad6a2061c 100644 --- a/source/common/listener_manager/filter_chain_manager_impl.cc +++ b/source/common/listener_manager/filter_chain_manager_impl.cc @@ -1,6 +1,7 @@ #include "source/common/listener_manager/filter_chain_manager_impl.h" #include "envoy/config/listener/v3/listener_components.pb.h" +#include "envoy/extensions/transport_sockets/raw_buffer/v3/raw_buffer.pb.h" #include "source/common/common/cleanup.h" #include "source/common/common/empty_string.h" @@ -14,6 +15,10 @@ #include "source/common/protobuf/utility.h" #include "source/server/configuration_impl.h" +#ifdef ENVOY_ENABLE_QUIC +#include "source/common/quic/quic_server_transport_socket_factory.h" +#endif + #include "absl/container/node_hash_map.h" #include "absl/strings/match.h" #include "absl/strings/str_cat.h" @@ -53,7 +58,7 @@ class FilterChainNameActionFactory : public Matcher::ActionFactory( - dynamic_cast(config).value()); + Envoy::Protobuf::DynamicCastMessage(config).value()); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); @@ -77,7 +82,7 @@ class FilterChainNameActionValidationVisitor PerFilterChainFactoryContextImpl::PerFilterChainFactoryContextImpl( Configuration::FactoryContext& parent_context, Init::Manager& init_manager) : parent_context_(parent_context), scope_(parent_context_.scope().createScope("")), - filter_chain_scope_(parent_context_.listenerScope().createScope("")), + prefixed_scope_(parent_context_.prefixedScope().createScope("")), init_manager_(init_manager) {} bool PerFilterChainFactoryContextImpl::drainClose(Network::DrainDirection scope) const { @@ -88,21 +93,24 @@ Network::DrainDecision& PerFilterChainFactoryContextImpl::drainDecision() { retu Init::Manager& PerFilterChainFactoryContextImpl::initManager() { return init_manager_; } -const Network::ListenerInfo& PerFilterChainFactoryContextImpl::listenerInfo() const { - return parent_context_.listenerInfo(); -} - ProtobufMessage::ValidationVisitor& PerFilterChainFactoryContextImpl::messageValidationVisitor() { return parent_context_.messageValidationVisitor(); } Stats::Scope& PerFilterChainFactoryContextImpl::scope() { return *scope_; } +Stats::Scope& PerFilterChainFactoryContextImpl::prefixedScope() { return *prefixed_scope_; } Configuration::ServerFactoryContext& PerFilterChainFactoryContextImpl::serverFactoryContext() { return parent_context_.serverFactoryContext(); } -Stats::Scope& PerFilterChainFactoryContextImpl::listenerScope() { return *filter_chain_scope_; } +envoy::config::core::v3::TrafficDirection PerFilterChainFactoryContextImpl::direction() const { + return parent_context_.direction(); +} +bool PerFilterChainFactoryContextImpl::isQuic() const { return parent_context_.isQuic(); } +bool PerFilterChainFactoryContextImpl::shouldBypassOverloadManager() const { + return parent_context_.shouldBypassOverloadManager(); +} FilterChainManagerImpl::FilterChainManagerImpl( const std::vector& addresses, @@ -121,7 +129,7 @@ absl::Status FilterChainManagerImpl::addFilterChains( const envoy::config::listener::v3::FilterChain* default_filter_chain, FilterChainFactoryBuilder& filter_chain_factory_builder, FilterChainFactoryContextCreator& context_creator) { - Cleanup cleanup([this]() { origin_ = absl::nullopt; }); + Cleanup cleanup([this]() { origin_ = std::nullopt; }); FilterChainsByMatcher filter_chains; uint32_t new_filter_chain_size = 0; FilterChainsByName filter_chains_by_name; @@ -299,7 +307,7 @@ absl::Status FilterChainManagerImpl::copyOrRebuildDefaultFilterChain( if (default_filter_chain == nullptr) { return absl::OkStatus(); } - default_filter_chain_message_ = absl::make_optional(*default_filter_chain); + default_filter_chain_message_ = std::make_optional(*default_filter_chain); // Origin filter chain manager could be empty if the current is the ancestor. const auto* origin = getOriginFilterChainManager(); @@ -847,5 +855,85 @@ Configuration::FilterChainFactoryContextPtr FilterChainManagerImpl::createFilter return std::make_unique(parent_context_, init_manager_); } +ListenerFilterChainFactoryBuilder::ListenerFilterChainFactoryBuilder( + bool is_quic, ProtobufMessage::ValidationVisitor& validator, + ListenerComponentFactory& listener_component_factory, + Server::Configuration::TransportSocketFactoryContext& factory_context) + : is_quic_(is_quic), validator_(validator), + listener_component_factory_(listener_component_factory), factory_context_(factory_context) {} + +absl::StatusOr +ListenerFilterChainFactoryBuilder::buildFilterChain( + const envoy::config::listener::v3::FilterChain& filter_chain, + FilterChainFactoryContextCreator& context_creator, bool added_via_api) const { + return buildFilterChainInternal( + filter_chain, context_creator.createFilterChainFactoryContext(&filter_chain), added_via_api); +} + +absl::StatusOr +ListenerFilterChainFactoryBuilder::buildFilterChainInternal( + const envoy::config::listener::v3::FilterChain& filter_chain, + Configuration::FilterChainFactoryContextPtr&& filter_chain_factory_context, + bool added_via_api) const { + // If the cluster doesn't have transport socket configured, then use the default "raw_buffer" + // transport socket or BoringSSL-based "tls" transport socket if TLS settings are configured. + // We copy by value first then override if necessary. + auto transport_socket = filter_chain.transport_socket(); + if (!filter_chain.has_transport_socket()) { + envoy::extensions::transport_sockets::raw_buffer::v3::RawBuffer raw_buffer; + std::ignore = transport_socket.mutable_typed_config()->PackFrom(raw_buffer); + transport_socket.set_name("envoy.transport_sockets.raw_buffer"); + } + + auto& config_factory = Config::Utility::getAndCheckFactory< + Server::Configuration::DownstreamTransportSocketConfigFactory>(transport_socket); +#if defined(ENVOY_ENABLE_QUIC) + if (is_quic_ && + dynamic_cast(&config_factory) == nullptr) { + return absl::InvalidArgumentError( + fmt::format("error building filter chain for quic listener: wrong " + "transport socket config specified for quic transport socket: " + "{}. \nUse QuicDownstreamTransport instead.", + transport_socket.DebugString())); + } + const std::string hcm_str = + "type.googleapis.com/" + "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; + if (is_quic_ && + (filter_chain.filters().empty() || + filter_chain.filters(filter_chain.filters().size() - 1).typed_config().type_url() != + hcm_str)) { + return absl::InvalidArgumentError( + fmt::format("error building network filter chain for quic listener: requires " + "http_connection_manager filter to be last in the chain.")); + } +#else + // When QUIC is compiled out it should not be possible to configure either the QUIC transport + // socket or the QUIC listener and get to this point. + ASSERT(!is_quic_); +#endif + ProtobufTypes::MessagePtr message = + Config::Utility::translateToFactoryConfig(transport_socket, validator_, config_factory); + + std::vector server_names(filter_chain.filter_chain_match().server_names().begin(), + filter_chain.filter_chain_match().server_names().end()); + + auto factory_or_error = config_factory.createTransportSocketFactory(*message, factory_context_, + std::move(server_names)); + RETURN_IF_NOT_OK(factory_or_error.status()); + auto factory_list_or_error = listener_component_factory_.createNetworkFilterFactoryList( + filter_chain.filters(), *filter_chain_factory_context); + RETURN_IF_NOT_OK(factory_list_or_error.status()); + + auto filter_chain_res = std::make_shared( + std::move(factory_or_error.value()), std::move(*factory_list_or_error), + std::chrono::milliseconds( + PROTOBUF_GET_MS_OR_DEFAULT(filter_chain, transport_socket_connect_timeout, 0)), + added_via_api, filter_chain); + + filter_chain_res->setFilterChainFactoryContext(std::move(filter_chain_factory_context)); + return filter_chain_res; +} + } // namespace Server } // namespace Envoy diff --git a/source/common/listener_manager/filter_chain_manager_impl.h b/source/common/listener_manager/filter_chain_manager_impl.h index 015a5cae11bcb..bd596b952e2a6 100644 --- a/source/common/listener_manager/filter_chain_manager_impl.h +++ b/source/common/listener_manager/filter_chain_manager_impl.h @@ -64,10 +64,12 @@ class PerFilterChainFactoryContextImpl : public Configuration::FilterChainFactor Network::DrainDecision& drainDecision() override; Init::Manager& initManager() override; Stats::Scope& scope() override; - const Network::ListenerInfo& listenerInfo() const override; + Stats::Scope& prefixedScope() override; ProtobufMessage::ValidationVisitor& messageValidationVisitor() override; Configuration::ServerFactoryContext& serverFactoryContext() override; - Stats::Scope& listenerScope() override; + envoy::config::core::v3::TrafficDirection direction() const override; + bool isQuic() const override; + bool shouldBypassOverloadManager() const override; void startDraining() override { is_draining_.store(true); } @@ -75,8 +77,7 @@ class PerFilterChainFactoryContextImpl : public Configuration::FilterChainFactor Configuration::FactoryContext& parent_context_; // The scope that has empty prefix. Stats::ScopeSharedPtr scope_; - // filter_chain_scope_ has the same prefix as listener owners scope. - Stats::ScopeSharedPtr filter_chain_scope_; + Stats::ScopeSharedPtr prefixed_scope_; Init::Manager& init_manager_; std::atomic is_draining_{false}; }; @@ -211,8 +212,7 @@ class FilterChainManagerImpl : public Network::FilterChainManager, // Return the current view of filter chains, keyed by filter chain message. Used by the owning // listener to calculate the intersection of filter chains with another listener. const FcContextMap& filterChainsByMessage() const { return fc_contexts_; } - const absl::optional& - defaultFilterChainMessage() const { + const std::optional& defaultFilterChainMessage() const { return default_filter_chain_message_; } const Network::DrainableFilterChainSharedPtr& defaultFilterChain() const { @@ -365,7 +365,7 @@ class FilterChainManagerImpl : public Network::FilterChainManager, // detect the filter chains in the intersection of existing listener and new listener. FcContextMap fc_contexts_; - absl::optional default_filter_chain_message_; + std::optional default_filter_chain_message_; // The optional fallback filter chain if destination_ports_map_ does not find a matched filter // chain. Network::DrainableFilterChainSharedPtr default_filter_chain_; @@ -381,7 +381,7 @@ class FilterChainManagerImpl : public Network::FilterChainManager, // Reference to the previous generation of filter chain manager to share the filter chains. // Caution: only during warm up could the optional have value. - absl::optional origin_{nullptr}; + std::optional origin_{nullptr}; // For FilterChainFactoryContextCreator // init manager owned by the corresponding listener. The reference is valid when building the @@ -402,5 +402,29 @@ namespace FilterChain { DECLARE_FACTORY(FilterChainNameActionFactory); } +class ListenerFilterChainFactoryBuilder : public FilterChainFactoryBuilder { +public: + ListenerFilterChainFactoryBuilder( + bool is_quic, ProtobufMessage::ValidationVisitor& validator, + ListenerComponentFactory& listener_component_factory, + Server::Configuration::TransportSocketFactoryContext& factory_context); + + absl::StatusOr + buildFilterChain(const envoy::config::listener::v3::FilterChain& filter_chain, + FilterChainFactoryContextCreator& context_creator, + bool added_via_api) const override; + +private: + absl::StatusOr buildFilterChainInternal( + const envoy::config::listener::v3::FilterChain& filter_chain, + Configuration::FilterChainFactoryContextPtr&& filter_chain_factory_context, + bool added_via_api) const; + + const bool is_quic_; + ProtobufMessage::ValidationVisitor& validator_; + ListenerComponentFactory& listener_component_factory_; + Configuration::TransportSocketFactoryContext& factory_context_; +}; + } // namespace Server } // namespace Envoy diff --git a/source/common/listener_manager/lds_api.cc b/source/common/listener_manager/lds_api.cc index b5059231e8d21..79a4035a681d2 100644 --- a/source/common/listener_manager/lds_api.cc +++ b/source/common/listener_manager/lds_api.cc @@ -26,21 +26,22 @@ LdsApiImpl::LdsApiImpl(const envoy::config::core::v3::ConfigSource& lds_config, Config::XdsManager& xds_manager, Upstream::ClusterManager& cm, Init::Manager& init_manager, Stats::Scope& scope, ListenerManager& lm, ProtobufMessage::ValidationVisitor& validation_visitor) - : Envoy::Config::SubscriptionBase(validation_visitor, - "name"), - listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), - xds_manager_(xds_manager), init_target_("LDS", [this]() { subscription_->start({}); }) { - const auto resource_name = getResourceName(); + : listener_manager_(lm), scope_(scope.createScope("listener_manager.lds.")), + resource_type_helper_(validation_visitor, "name"), xds_manager_(xds_manager), + init_target_("LDS", [this]() { subscription_->start({}); }) { + const auto resource_name = resource_type_helper_.getResourceName(); if (lds_resources_locator == nullptr) { - subscription_ = THROW_OR_RETURN_VALUE(cm.subscriptionFactory().subscriptionFromConfigSource( - lds_config, Grpc::Common::typeUrl(resource_name), - *scope_, *this, resource_decoder_, {}), - Config::SubscriptionPtr); + subscription_ = + THROW_OR_RETURN_VALUE(cm.subscriptionFactory().subscriptionFromConfigSource( + lds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, + resource_type_helper_.resourceDecoder(), {}), + Config::SubscriptionPtr); } else { - subscription_ = THROW_OR_RETURN_VALUE( - cm.subscriptionFactory().collectionSubscriptionFromUrl( - *lds_resources_locator, lds_config, resource_name, *scope_, *this, resource_decoder_), - Config::SubscriptionPtr); + subscription_ = + THROW_OR_RETURN_VALUE(cm.subscriptionFactory().collectionSubscriptionFromUrl( + *lds_resources_locator, lds_config, resource_name, *scope_, *this, + resource_type_helper_.resourceDecoder()), + Config::SubscriptionPtr); } init_manager.add(init_target_); } @@ -81,14 +82,16 @@ LdsApiImpl::onConfigUpdate(const std::vector& added_ auto& state = failure_state.back(); state.set_details(error_message); #if defined(ENVOY_ENABLE_FULL_PROTOS) - state.mutable_failed_configuration()->PackFrom(resource.get().resource()); + std::ignore = state.mutable_failed_configuration()->PackFrom(resource.get().resource()); #endif absl::StrAppend(&message, listener_name, ": ", error_message, "\n"); + ENVOY_LOG(warn, "lds: listener '{}' config rejected: {}", listener_name, error_message); }; TRY_ASSERT_MAIN_THREAD { const envoy::config::listener::v3::Listener& listener = - dynamic_cast(resource.get().resource()); + Envoy::Protobuf::DynamicCastMessage( + resource.get().resource()); listener_name = listener.name(); if (!listener_names.insert(listener.name()).second) { // NOTE: at this point, the first of these duplicates has already been successfully diff --git a/source/common/listener_manager/lds_api.h b/source/common/listener_manager/lds_api.h index 1500441182c80..c453ef282e31c 100644 --- a/source/common/listener_manager/lds_api.h +++ b/source/common/listener_manager/lds_api.h @@ -13,7 +13,7 @@ #include "envoy/stats/scope.h" #include "source/common/common/logger.h" -#include "source/common/config/subscription_base.h" +#include "source/common/config/resource_type_helper.h" #include "source/common/init/target_impl.h" namespace Envoy { @@ -23,7 +23,7 @@ namespace Server { * LDS API implementation that fetches via Subscription. */ class LdsApiImpl : public LdsApi, - Envoy::Config::SubscriptionBase, + public Config::SubscriptionCallbacks, Logger::Loggable { public: LdsApiImpl(const envoy::config::core::v3::ConfigSource& lds_config, @@ -49,6 +49,7 @@ class LdsApiImpl : public LdsApi, std::string system_version_info_; ListenerManager& listener_manager_; Stats::ScopeSharedPtr scope_; + const Config::ResourceTypeHelper resource_type_helper_; Config::XdsManager& xds_manager_; Init::TargetImpl init_target_; }; diff --git a/source/common/listener_manager/listener_impl.cc b/source/common/listener_manager/listener_impl.cc index 8883d2415946a..82ebb4590ec53 100644 --- a/source/common/listener_manager/listener_impl.cc +++ b/source/common/listener_manager/listener_impl.cc @@ -10,6 +10,7 @@ #include "envoy/extensions/filters/listener/proxy_protocol/v3/proxy_protocol.pb.h" #include "envoy/extensions/udp_packet_writer/v3/udp_default_writer_factory.pb.h" #include "envoy/network/exception.h" +#include "envoy/network/udp_packet_writer_factory_factory.h" #include "envoy/registry/registry.h" #include "envoy/server/options.h" #include "envoy/server/transport_socket_config.h" @@ -457,7 +458,7 @@ ListenerImpl::ListenerImpl(const envoy::config::listener::v3::Listener& config, } } - const absl::optional runtime_val = + const std::optional runtime_val = listener_factory_context_->serverFactoryContext().runtime().snapshot().get( cx_limit_runtime_key_); if (runtime_val && runtime_val->empty()) { @@ -701,7 +702,7 @@ ListenerImpl::buildUdpListenerFactory(const envoy::config::listener::v3::Listene auto* factory_factory = Config::Utility::getFactory( config.udp_listener_config().udp_packet_packet_writer_config()); udp_listener_config_->writer_factory_ = factory_factory->createUdpPacketWriterFactory( - config.udp_listener_config().udp_packet_packet_writer_config()); + config.udp_listener_config().udp_packet_packet_writer_config(), *listener_factory_context_); } if (config.udp_listener_config().has_quic_options()) { #ifdef ENVOY_ENABLE_QUIC @@ -763,6 +764,11 @@ void ListenerImpl::buildListenSocketOptions( if (reuse_port_) { addListenSocketOptions(listen_socket_options_list_[i], Network::SocketOptionFactory::buildReusePortOptions()); + if (reusePortBpfCpuSteeringEnabled(config)) { + addListenSocketOptions(listen_socket_options_list_[i], + Network::SocketOptionFactory::buildReusePortBpfCpuSteeringOptions( + parent_.workerCpus())); + } } if (!address_opts_list[i]->empty()) { addListenSocketOptions(listen_socket_options_list_[i], address_opts_list[i]); @@ -866,7 +872,11 @@ ListenerImpl::validateFilterChains(const envoy::config::listener::v3::Listener& absl::Status ListenerImpl::buildFilterChains(const envoy::config::listener::v3::Listener& config) { transport_factory_context_->setInitManager(*dynamic_init_manager_); - ListenerFilterChainFactoryBuilder builder(*this, *transport_factory_context_); + // The only connection oriented UDP transport protocol right now is QUIC. + const bool is_quic = udpListenerConfig().has_value() && + !udpListenerConfig()->listenerFactory().isTransportConnectionless(); + ListenerFilterChainFactoryBuilder builder(is_quic, validation_visitor_, *parent_.factory_, + *transport_factory_context_); return filter_chain_manager_->addFilterChains( config.has_filter_chain_matcher() ? &config.filter_chain_matcher() : nullptr, config.filter_chains(), @@ -874,6 +884,18 @@ absl::Status ListenerImpl::buildFilterChains(const envoy::config::listener::v3:: *filter_chain_manager_); } +bool ListenerImpl::reusePortBpfCpuSteeringEnabled( + const envoy::config::listener::v3::Listener& config) const { + // Steering maps each receiving CPU to the worker pinned to it. The listener manager tracks the + // global prerequisites (worker CPU affinity and kernel support) once for all workers. The mapping + // is fixed at startup, so a listener added later via LDS can steer too. + return socket_type_ == Network::Socket::Type::Stream && reuse_port_ && + config.has_connection_balance_config() && + config.connection_balance_config().balance_type_case() == + envoy::config::listener::v3::Listener_ConnectionBalanceConfig::kCpuLocalityBalance && + parent_.reusePortBpfCpuSteeringSupported(); +} + absl::Status ListenerImpl::buildConnectionBalancer(const envoy::config::listener::v3::Listener& config, const Network::Address::Instance& address) { @@ -917,6 +939,21 @@ ListenerImpl::buildConnectionBalancer(const envoy::config::listener::v3::Listene config.connection_balance_config().extend_balance(), *listener_factory_context_)); break; } + case envoy::config::listener::v3::Listener_ConnectionBalanceConfig::kCpuLocalityBalance: + // CPU locality balancing is performed by the kernel reuse port BPF program installed as a + // listen socket option, so no user space balancer is needed and the no-op balancer is + // always used. When steering is not available the kernel distributes connections with its + // default reuse port hashing instead. + if (!reusePortBpfCpuSteeringEnabled(config)) { + ENVOY_LOG(warn, + "the CPU locality connection balancer is configured for TCP listener '{}' but " + "reuse port BPF CPU steering is not active, so new connections are not steered " + "to CPU-local workers.", + config.name()); + } + connection_balancers_.emplace(address.asString(), + std::make_shared()); + break; case envoy::config::listener::v3::Listener_ConnectionBalanceConfig::BALANCE_TYPE_NOT_SET: { return absl::InvalidArgumentError("No valid balance type for connection balance"); } @@ -1004,6 +1041,18 @@ ProtobufMessage::ValidationVisitor& PerListenerFactoryContextImpl::messageValida Configuration::ServerFactoryContext& PerListenerFactoryContextImpl::serverFactoryContext() { return listener_factory_context_base_->serverFactoryContext(); } +envoy::config::core::v3::TrafficDirection PerListenerFactoryContextImpl::direction() const { + return listener_factory_context_base_->listenerInfo().direction(); +} +bool PerListenerFactoryContextImpl::isQuic() const { + return listener_factory_context_base_->listenerInfo().isQuic(); +} +bool PerListenerFactoryContextImpl::shouldBypassOverloadManager() const { + return listener_factory_context_base_->listenerInfo().shouldBypassOverloadManager(); +} +Stats::Scope& PerListenerFactoryContextImpl::prefixedScope() { + return listener_factory_context_base_->listenerScope(); +} Stats::Scope& PerListenerFactoryContextImpl::listenerScope() { return listener_factory_context_base_->listenerScope(); } @@ -1050,7 +1099,7 @@ bool ListenerImpl::createQuicListenerFilterChain(Network::QuicListenerFilterMana } void ListenerImpl::dumpListenerConfig(Protobuf::Any& dump) const { - dump.PackFrom(config_maybe_partial_filter_chains_); + std::ignore = dump.PackFrom(config_maybe_partial_filter_chains_); } void ListenerImpl::debugLog(const std::string& message) { diff --git a/source/common/listener_manager/listener_impl.h b/source/common/listener_manager/listener_impl.h index 416728bd96223..4e89f24cde754 100644 --- a/source/common/listener_manager/listener_impl.h +++ b/source/common/listener_manager/listener_impl.h @@ -184,7 +184,11 @@ class PerListenerFactoryContextImpl : public Configuration::ListenerFactoryConte const Network::ListenerInfo& listenerInfo() const override; ProtobufMessage::ValidationVisitor& messageValidationVisitor() override; Configuration::ServerFactoryContext& serverFactoryContext() override; + envoy::config::core::v3::TrafficDirection direction() const override; + bool isQuic() const override; + bool shouldBypassOverloadManager() const override; + Stats::Scope& prefixedScope() override; Stats::Scope& listenerScope() override; ListenerFactoryContextBaseImpl& parentFactoryContext() { return *listener_factory_context_base_; } @@ -420,6 +424,11 @@ class ListenerImpl final : public Network::ListenerConfig, absl::Status buildFilterChains(const envoy::config::listener::v3::Listener& config); absl::Status buildConnectionBalancer(const envoy::config::listener::v3::Listener& config, const Network::Address::Instance& address); + // Returns true when the listener should steer connections to workers using a reuse port BPF + // program. Requires a TCP listener with reuse port enabled, the CPU locality balancer configured, + // worker CPU affinity pinning every worker to a CPU, and a kernel that supports reuse port BPF + // steering. + bool reusePortBpfCpuSteeringEnabled(const envoy::config::listener::v3::Listener& config) const; void buildSocketOptions(const envoy::config::listener::v3::Listener& config); void buildOriginalDstListenerFilter(const envoy::config::listener::v3::Listener& config); void buildProxyProtocolListenerFilter(const envoy::config::listener::v3::Listener& config); diff --git a/source/common/listener_manager/listener_manager_impl.cc b/source/common/listener_manager/listener_manager_impl.cc index 7d09ce10050b6..66dffdf2e5c49 100644 --- a/source/common/listener_manager/listener_manager_impl.cc +++ b/source/common/listener_manager/listener_manager_impl.cc @@ -16,7 +16,9 @@ #include "source/common/api/os_sys_calls_impl.h" #include "source/common/common/assert.h" +#include "source/common/common/cpu_affinity.h" #include "source/common/common/fmt.h" +#include "source/common/common/thread.h" #include "source/common/config/utility.h" #include "source/common/network/filter_matcher.h" #include "source/common/network/io_socket_handle_impl.h" @@ -26,7 +28,9 @@ #include "source/common/network/utility.h" #include "source/common/protobuf/utility.h" +#include "absl/strings/str_join.h" #include "absl/synchronization/blocking_counter.h" +#include "absl/types/span.h" #if defined(ENVOY_ENABLE_QUIC) #include "source/common/quic/quic_server_transport_socket_factory.h" @@ -335,7 +339,7 @@ absl::StatusOr ProdListenerComponentFactory::createLis return absl::InvalidArgumentError("failed to create socket using custom interface"); } return std::make_shared(std::move(io_handle), address, options, - absl::nullopt, bind_type != BindType::NoBind); + std::nullopt, bind_type != BindType::NoBind); } // Continue with standard socket creation for addresses using the default interface. @@ -566,7 +570,7 @@ ListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::List TimestampUtil::systemClockToTimestamp(server_.api().timeSource().systemTime(), *(it->second->mutable_last_update_attempt())); it->second->set_details(add_or_update_status.status().message()); - it->second->mutable_failed_configuration()->PackFrom(config); + std::ignore = it->second->mutable_failed_configuration()->PackFrom(config); } return add_or_update_status; } @@ -741,6 +745,13 @@ void ListenerManagerImpl::drainListener(ListenerImplPtr&& listener) { } }); + // Notify existing connections on this listener that draining has begun so that callbacks + // (e.g. HTTP/2 codecs) can react before the drain timer expires and connections are + // forcibly closed. + for (const auto& worker : workers_) { + worker->onListenerDrain(*draining_it->listener_); + } + // Start the drain sequence which completes when the listener's drain manager has completed // draining at whatever the server configured drain times are. draining_it->listener_->localDrainManager().startDrainSequence( @@ -830,7 +841,7 @@ bool ListenerManagerImpl::doFinalPreWorkerListenerInit(ListenerImpl& listener) { } void ListenerManagerImpl::addListenerToWorker(Worker& worker, - absl::optional overridden_listener, + std::optional overridden_listener, ListenerImpl& listener, ListenerCompletionCallback completion_callback) { if (overridden_listener.has_value()) { @@ -862,7 +873,7 @@ void ListenerManagerImpl::onListenerWarmed(ListenerImpl& listener) { return; } for (const auto& worker : workers_) { - addListenerToWorker(*worker, absl::nullopt, listener, nullptr); + addListenerToWorker(*worker, std::nullopt, listener, nullptr); } auto existing_active_listener = getListenerByName(active_listeners_, listener.name()); @@ -932,6 +943,14 @@ void ListenerManagerImpl::drainFilterChains(ListenerImplPtr&& draining_listener, absl::StrCat("draining ", filter_chain_size, " filter chains in listener ", draining_group->getDrainingListener().name())); + // Notify existing connections in the draining filter chains that draining has begun so + // callbacks (e.g. HTTP/2 codecs) can react before the drain timer expires and the + // connections are forcibly closed. + for (const auto& worker : workers_) { + worker->onFilterChainDrain(draining_group->getDrainingListenerTag(), + draining_group->getDrainingFilterChains()); + } + // Start the drain sequence which completes when the listener's drain manager has completed // draining at whatever the server configured drain times are. draining_group->startDrainSequence( @@ -1018,6 +1037,31 @@ bool ListenerManagerImpl::removeListenerInternal(const std::string& name, return true; } +absl::Span ListenerManagerImpl::workerCpus() { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + // Pin each worker thread to a CPU when worker CPU affinity is enabled so each worker keeps its + // cache and `NUMA` locality. The assignment depends only on the worker count and the process + // affinity mask, both fixed at startup, so it is computed once and cached for reuse when building + // listeners and when starting workers. + if (!worker_cpus_.has_value()) { + worker_cpus_ = server_.bootstrap().enable_worker_cpu_affinity() + ? Thread::workerCpuAssignment(workers_.size()) + : std::vector{}; + } + return *worker_cpus_; +} + +bool ListenerManagerImpl::reusePortBpfCpuSteeringSupported() { + ASSERT_IS_MAIN_OR_TEST_THREAD(); + // Steering needs every worker pinned to a CPU and a kernel that supports the program. Both inputs + // are fixed at startup, so the result is computed once and cached. + if (!reuse_port_bpf_cpu_steering_supported_.has_value()) { + reuse_port_bpf_cpu_steering_supported_ = + !workerCpus().empty() && Api::OsSysCallsSingleton::get().supportsReusePortBpfCpuSteering(); + } + return *reuse_port_bpf_cpu_steering_supported_; +} + absl::Status ListenerManagerImpl::startWorkers(OptRef guard_dog, std::function callback) { ENVOY_LOG(info, "all dependencies initialized. starting workers"); @@ -1050,7 +1094,7 @@ absl::Status ListenerManagerImpl::startWorkers(OptRef guard_dog, continue; } for (const auto& worker : workers_) { - addListenerToWorker(*worker, absl::nullopt, *listener, + addListenerToWorker(*worker, std::nullopt, *listener, [this, listeners_pending_init, callback]() { if (--(*listeners_pending_init) == 0) { stats_.workers_started_.set(1); @@ -1059,9 +1103,27 @@ absl::Status ListenerManagerImpl::startWorkers(OptRef guard_dog, }); } } + const absl::Span worker_cpus = workerCpus(); + if (server_.bootstrap().enable_worker_cpu_affinity()) { + // The gauge counts workers assigned a CPU. Each assigned CPU is in the process mask so the + // per-worker pin normally succeeds, and a rare failure to set affinity is logged by + // `setThreadAffinity()`. + stats_.workers_pinned_.set(worker_cpus.size()); + if (worker_cpus.empty()) { + ENVOY_LOG(warn, "worker CPU affinity is enabled but no worker could be pinned for {} workers", + workers_.size()); + } else { + ENVOY_LOG(info, "worker CPU affinity is enabled, pinning {} workers to CPUs {}", + worker_cpus.size(), absl::StrJoin(worker_cpus, ",")); + } + } for (const auto& worker : workers_) { ENVOY_LOG(debug, "starting worker {}", i); - worker->start(guard_dog, worker_started_running); + std::optional cpu_id; + if (i < worker_cpus.size()) { + cpu_id = worker_cpus[i]; + } + worker->start(guard_dog, worker_started_running, cpu_id); if (enable_dispatcher_stats_) { worker->initializeStats(*scope_); } @@ -1139,89 +1201,6 @@ void ListenerManagerImpl::endListenerUpdate(FailureStates&& failure_states) { overall_error_state_ = std::move(failure_states); } -ListenerFilterChainFactoryBuilder::ListenerFilterChainFactoryBuilder( - ListenerImpl& listener, - Server::Configuration::TransportSocketFactoryContextImpl& factory_context) - : listener_(listener), validator_(listener.validation_visitor_), - listener_component_factory_(*listener.parent_.factory_), factory_context_(factory_context) {} - -absl::StatusOr -ListenerFilterChainFactoryBuilder::buildFilterChain( - const envoy::config::listener::v3::FilterChain& filter_chain, - FilterChainFactoryContextCreator& context_creator, bool added_via_api) const { - return buildFilterChainInternal( - filter_chain, context_creator.createFilterChainFactoryContext(&filter_chain), added_via_api); -} - -absl::StatusOr -ListenerFilterChainFactoryBuilder::buildFilterChainInternal( - const envoy::config::listener::v3::FilterChain& filter_chain, - Configuration::FilterChainFactoryContextPtr&& filter_chain_factory_context, - bool added_via_api) const { - // If the cluster doesn't have transport socket configured, then use the default "raw_buffer" - // transport socket or BoringSSL-based "tls" transport socket if TLS settings are configured. - // We copy by value first then override if necessary. - auto transport_socket = filter_chain.transport_socket(); - if (!filter_chain.has_transport_socket()) { - envoy::extensions::transport_sockets::raw_buffer::v3::RawBuffer raw_buffer; - transport_socket.mutable_typed_config()->PackFrom(raw_buffer); - transport_socket.set_name("envoy.transport_sockets.raw_buffer"); - } - - auto& config_factory = Config::Utility::getAndCheckFactory< - Server::Configuration::DownstreamTransportSocketConfigFactory>(transport_socket); - // The only connection oriented UDP transport protocol right now is QUIC. - const bool is_quic = - listener_.udpListenerConfig().has_value() && - !listener_.udpListenerConfig()->listenerFactory().isTransportConnectionless(); -#if defined(ENVOY_ENABLE_QUIC) - if (is_quic && - dynamic_cast(&config_factory) == nullptr) { - return absl::InvalidArgumentError( - fmt::format("error building filter chain for quic listener: wrong " - "transport socket config specified for quic transport socket: " - "{}. \nUse QuicDownstreamTransport instead.", - transport_socket.DebugString())); - } - const std::string hcm_str = - "type.googleapis.com/" - "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; - if (is_quic && - (filter_chain.filters().empty() || - filter_chain.filters(filter_chain.filters().size() - 1).typed_config().type_url() != - hcm_str)) { - return absl::InvalidArgumentError( - fmt::format("error building network filter chain for quic listener: requires " - "http_connection_manager filter to be last in the chain.")); - } -#else - // When QUIC is compiled out it should not be possible to configure either the QUIC transport - // socket or the QUIC listener and get to this point. - ASSERT(!is_quic); -#endif - ProtobufTypes::MessagePtr message = - Config::Utility::translateToFactoryConfig(transport_socket, validator_, config_factory); - - std::vector server_names(filter_chain.filter_chain_match().server_names().begin(), - filter_chain.filter_chain_match().server_names().end()); - - auto factory_or_error = config_factory.createTransportSocketFactory(*message, factory_context_, - std::move(server_names)); - RETURN_IF_NOT_OK(factory_or_error.status()); - auto factory_list_or_error = listener_component_factory_.createNetworkFilterFactoryList( - filter_chain.filters(), *filter_chain_factory_context); - RETURN_IF_NOT_OK(factory_list_or_error.status()); - - auto filter_chain_res = std::make_shared( - std::move(factory_or_error.value()), std::move(*factory_list_or_error), - std::chrono::milliseconds( - PROTOBUF_GET_MS_OR_DEFAULT(filter_chain, transport_socket_connect_timeout, 0)), - added_via_api, filter_chain); - - filter_chain_res->setFilterChainFactoryContext(std::move(filter_chain_factory_context)); - return filter_chain_res; -} - absl::Status ListenerManagerImpl::setNewOrDrainingSocketFactory(const std::string& name, ListenerImpl& listener) { if (hasListenerWithDuplicatedAddress(warming_listeners_, listener) || @@ -1342,7 +1321,7 @@ void ListenerManagerImpl::maybeCloseSocketsForListener(ListenerImpl& listener) { } ApiListenerOptRef ListenerManagerImpl::apiListener() { - return api_listener_ ? ApiListenerOptRef(std::ref(*api_listener_)) : absl::nullopt; + return api_listener_ ? ApiListenerOptRef(std::ref(*api_listener_)) : std::nullopt; } ListenerUpdateCallbacksHandlePtr diff --git a/source/common/listener_manager/listener_manager_impl.h b/source/common/listener_manager/listener_manager_impl.h index 6bd65b333e2c5..9b65e78f24451 100644 --- a/source/common/listener_manager/listener_manager_impl.h +++ b/source/common/listener_manager/listener_manager_impl.h @@ -4,6 +4,7 @@ #include #include "envoy/admin/v3/config_dump.pb.h" +#include "envoy/config/bootstrap/v3/bootstrap.pb.h" #include "envoy/config/core/v3/address.pb.h" #include "envoy/config/core/v3/base.pb.h" #include "envoy/config/core/v3/config_source.pb.h" @@ -29,6 +30,8 @@ #include "source/common/quic/quic_stat_names.h" #include "source/server/listener_manager_factory.h" +#include "absl/types/span.h" + namespace Envoy { namespace Server { @@ -36,8 +39,6 @@ namespace Configuration { using TransportSocketFactoryContextImpl = Server::GenericFactoryContextImpl; } -class ListenerFilterChainFactoryBuilder; - /** * Prod implementation of ListenerComponentFactory that creates real sockets and attempts to fetch * sockets from the parent process via the hot restarter. The filter factory list is created from @@ -160,7 +161,8 @@ using ListenerImplPtr = std::unique_ptr; GAUGE(total_listeners_active, NeverImport) \ GAUGE(total_listeners_draining, NeverImport) \ GAUGE(total_listeners_warming, NeverImport) \ - GAUGE(workers_started, NeverImport) + GAUGE(workers_started, NeverImport) \ + GAUGE(workers_pinned, NeverImport) /** * Struct definition for all listener manager stats. @see stats_macros.h @@ -241,12 +243,22 @@ class ListenerManagerImpl : public ListenerManager, Logger::Loggable workerCpus(); + + // Returns true when reuse port BPF CPU steering can be used, that is every worker is pinned to a + // CPU and the kernel supports the steering program. The result is computed once and cached. + bool reusePortBpfCpuSteeringSupported(); + Instance& server_; std::unique_ptr factory_; @@ -279,7 +291,7 @@ class ListenerManagerImpl : public ListenerManager, Logger::Loggable overridden_listener, + void addListenerToWorker(Worker& worker, std::optional overridden_listener, ListenerImpl& listener, ListenerCompletionCallback completion_callback); ProtobufTypes::MessagePtr dumpListenerConfigs(const Matchers::StringMatcher& name_matcher); @@ -371,8 +383,14 @@ class ListenerManagerImpl : public ListenerManager, Logger::Loggable draining_filter_chains_manager_; std::vector workers_; + // The per-worker CPU assignment, lazily computed and cached by workerCpus(). worker_cpus_[i] is + // the CPU that worker i is pinned to; the vector is empty when no worker is pinned. + std::optional> worker_cpus_; + // Whether reuse port BPF CPU steering is usable, lazily computed and cached by + // reusePortBpfCpuSteeringSupported(). + std::optional reuse_port_bpf_cpu_steering_supported_; bool workers_started_{}; - absl::optional stop_listeners_type_; + std::optional stop_listeners_type_; Stats::ScopeSharedPtr scope_; ListenerManagerStats stats_; ConfigTracker::EntryOwnerPtr listeners_config_tracker_entry_; @@ -386,28 +404,6 @@ class ListenerManagerImpl : public ListenerManager, Logger::Loggable update_callbacks_; }; -class ListenerFilterChainFactoryBuilder : public FilterChainFactoryBuilder { -public: - ListenerFilterChainFactoryBuilder( - ListenerImpl& listener, Configuration::TransportSocketFactoryContextImpl& factory_context); - - absl::StatusOr - buildFilterChain(const envoy::config::listener::v3::FilterChain& filter_chain, - FilterChainFactoryContextCreator& context_creator, - bool added_via_api) const override; - -private: - absl::StatusOr buildFilterChainInternal( - const envoy::config::listener::v3::FilterChain& filter_chain, - Configuration::FilterChainFactoryContextPtr&& filter_chain_factory_context, - bool added_via_api) const; - - ListenerImpl& listener_; - ProtobufMessage::ValidationVisitor& validator_; - ListenerComponentFactory& listener_component_factory_; - Configuration::TransportSocketFactoryContextImpl& factory_context_; -}; - class DefaultListenerManagerFactoryImpl : public ListenerManagerFactory { public: std::unique_ptr @@ -423,7 +419,7 @@ class DefaultListenerManagerFactoryImpl : public ListenerManagerFactory { return Config::ServerExtensionValues::get().DEFAULT_LISTENER; } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } }; diff --git a/source/common/local_reply/local_reply.cc b/source/common/local_reply/local_reply.cc index 2045a185a2410..113062bee8ea7 100644 --- a/source/common/local_reply/local_reply.cc +++ b/source/common/local_reply/local_reply.cc @@ -133,8 +133,8 @@ class ResponseMapper { private: const AccessLog::FilterPtr filter_; - absl::optional status_code_; - absl::optional body_; + std::optional status_code_; + std::optional body_; HeaderParserPtr header_parser_; BodyFormatterPtr body_formatter_; }; diff --git a/source/common/matcher/BUILD b/source/common/matcher/BUILD index 49d429889ec4b..9792d68641ea8 100644 --- a/source/common/matcher/BUILD +++ b/source/common/matcher/BUILD @@ -87,6 +87,18 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "address_matcher_lib", + srcs = ["address_matcher.cc"], + hdrs = ["address_matcher.h"], + deps = [ + "//envoy/common:exception_lib", + "//source/common/network:cidr_range_lib", + "@abseil-cpp//absl/status:statusor", + "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "regex_replace_lib", srcs = ["regex_replace.cc"], diff --git a/source/common/matcher/actions/string_returning_action.cc b/source/common/matcher/actions/string_returning_action.cc index 8f12178296176..06b9a9b1a1f99 100644 --- a/source/common/matcher/actions/string_returning_action.cc +++ b/source/common/matcher/actions/string_returning_action.cc @@ -2,7 +2,7 @@ #include "envoy/config/core/v3/substitution_format_string.pb.h" #include "envoy/config/core/v3/substitution_format_string.pb.validate.h" -#include "envoy/formatter/substitution_formatter_base.h" +#include "envoy/formatter/substitution_formatter.h" #include "envoy/registry/registry.h" #include "source/common/formatter/substitution_format_string.h" @@ -67,7 +67,7 @@ class StringReturningDirectActionFactory StringReturningActionFactoryContext&, ProtobufMessage::ValidationVisitor&) override { // validate function doesn't exist for StringValue, so just cast. - const auto& config = dynamic_cast(proto_config); + const auto& config = Envoy::Protobuf::DynamicCastMessage(proto_config); return std::make_shared(config); } ProtobufTypes::MessagePtr createEmptyConfigProto() override { diff --git a/source/common/matcher/address_matcher.cc b/source/common/matcher/address_matcher.cc new file mode 100644 index 0000000000000..53fee3adf5aba --- /dev/null +++ b/source/common/matcher/address_matcher.cc @@ -0,0 +1,25 @@ +#include "source/common/matcher/address_matcher.h" + +#include "envoy/common/exception.h" + +namespace Envoy { +namespace Matcher { + +absl::StatusOr> +AddressMatcher::create(const envoy::type::matcher::v3::AddressMatcher& matcher) { + auto ip_list = Network::Address::IpList::create(matcher.ranges()); + RETURN_IF_NOT_OK_REF(ip_list.status()); + return std::make_unique(std::move(*ip_list), matcher.invert_match()); +} + +AddressMatcher::AddressMatcher(std::unique_ptr&& ip_list, + bool invert_match) + : ip_list_(std::move(ip_list)), invert_match_(invert_match) {} + +bool AddressMatcher::match(const Network::Address::Instance& address) const { + const bool matches = ip_list_->contains(address); + return invert_match_ ? !matches : matches; +} + +} // namespace Matcher +} // namespace Envoy diff --git a/source/common/matcher/address_matcher.h b/source/common/matcher/address_matcher.h new file mode 100644 index 0000000000000..736dbdd6d2946 --- /dev/null +++ b/source/common/matcher/address_matcher.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "envoy/type/matcher/v3/address.pb.h" + +#include "source/common/network/cidr_range.h" + +#include "absl/status/statusor.h" + +namespace Envoy { +namespace Matcher { + +/** + * Matches an IP address against a list of CIDR ranges, with optional inversion. + * This is the C++ implementation of the envoy.type.matcher.v3.AddressMatcher proto. + */ +class AddressMatcher { +public: + /** + * Creates an AddressMatcher from the proto configuration. + */ + static absl::StatusOr> + create(const envoy::type::matcher::v3::AddressMatcher& matcher); + + AddressMatcher(std::unique_ptr&& ip_list, bool invert_match = false); + + /** + * Returns true if the address matches the configured CIDR ranges (respecting invert_match). + */ + bool match(const Network::Address::Instance& address) const; + +private: + const std::unique_ptr ip_list_; + const bool invert_match_; +}; + +using AddressMatcherPtr = std::unique_ptr; + +} // namespace Matcher +} // namespace Envoy diff --git a/source/common/matcher/exact_map_matcher.h b/source/common/matcher/exact_map_matcher.h index aa0a0759ca8dd..8ade6077df173 100644 --- a/source/common/matcher/exact_map_matcher.h +++ b/source/common/matcher/exact_map_matcher.h @@ -12,7 +12,7 @@ namespace Matcher { template class ExactMapMatcher : public MapMatcher { public: static absl::StatusOr> - create(DataInputPtr&& data_input, absl::optional> on_no_match) { + create(DataInputPtr&& data_input, std::optional> on_no_match) { absl::Status creation_status = absl::OkStatus(); auto ret = std::unique_ptr>( new ExactMapMatcher(std::move(data_input), on_no_match, creation_status)); @@ -28,8 +28,8 @@ template class ExactMapMatcher : public MapMatcher { protected: template friend class MatchTreeFactory; - ExactMapMatcher(DataInputPtr&& data_input, - absl::optional> on_no_match, absl::Status& creation_status) + ExactMapMatcher(DataInputPtr&& data_input, std::optional> on_no_match, + absl::Status& creation_status) : MapMatcher(std::move(data_input), std::move(on_no_match), creation_status) {} ActionMatchResult doMatch(const DataType& data, absl::string_view key, diff --git a/source/common/matcher/field_matcher.h b/source/common/matcher/field_matcher.h index 00e1fe7e08804..a09c5e4cc4471 100644 --- a/source/common/matcher/field_matcher.h +++ b/source/common/matcher/field_matcher.h @@ -25,7 +25,7 @@ template using FieldMatcherPtr = std::unique_ptr class AllFieldMatcher : public FieldMatcher { public: @@ -59,7 +59,7 @@ template class AllFieldMatcher : public FieldMatcher * FieldMatchers evaluate to true. * * If any of the underlying FieldMatchers are unable to produce a result before we see a successful - * match, absl::nullopt is returned. + * match, std::nullopt is returned. */ template class AnyFieldMatcher : public FieldMatcher { public: diff --git a/source/common/matcher/list_matcher.h b/source/common/matcher/list_matcher.h index ecbaa6d448eda..2596cd3035ff6 100644 --- a/source/common/matcher/list_matcher.h +++ b/source/common/matcher/list_matcher.h @@ -13,7 +13,7 @@ namespace Matcher { */ template class ListMatcher : public MatchTree { public: - explicit ListMatcher(absl::optional> on_no_match) : on_no_match_(on_no_match) {} + explicit ListMatcher(std::optional> on_no_match) : on_no_match_(on_no_match) {} ActionMatchResult match(const DataType& matching_data, SkippedMatchCb skipped_match_cb = nullptr) override { @@ -47,7 +47,7 @@ template class ListMatcher : public MatchTree { } private: - absl::optional> on_no_match_; + std::optional> on_no_match_; std::vector, OnMatch>> matchers_; }; diff --git a/source/common/matcher/map_matcher.h b/source/common/matcher/map_matcher.h index edc23fb372f34..e2670d2deabbf 100644 --- a/source/common/matcher/map_matcher.h +++ b/source/common/matcher/map_matcher.h @@ -17,6 +17,9 @@ class MapMatcher : public MatchTree, Logger::Loggable&& on_match) PURE; ActionMatchResult doNoMatch(const DataType& data, SkippedMatchCb skipped_match_cb) { + // This is not true redundant-smartptr-get but we still add a NOLINT to make the + // clang-tidy check happy. + // NOLINTNEXTLINE(readability-redundant-smartptr-get) if (data_input_->get(data).availability() == DataAvailability::MoreDataMightBeAvailable) { return ActionMatchResult::insufficientData(); } @@ -41,7 +44,7 @@ class MapMatcher : public MatchTree, Logger::Loggable friend class MatchTreeFactory; - MapMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, + MapMatcher(DataInputPtr&& data_input, std::optional> on_no_match, absl::Status& creation_status) : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)) { auto input_type = data_input_->dataInputType(); @@ -53,7 +56,7 @@ class MapMatcher : public MatchTree, Logger::Loggable data_input_; - const absl::optional> on_no_match_; + const std::optional> on_no_match_; // The inner match method. Attempts to match against the resulting data string. // If a match is found, handleRecursionAndSkips must be called on it. diff --git a/source/common/matcher/matcher.h b/source/common/matcher/matcher.h index 416527fff6a10..b8d2760f92b38 100644 --- a/source/common/matcher/matcher.h +++ b/source/common/matcher/matcher.h @@ -2,6 +2,7 @@ #include #include +#include #include #include "envoy/config/common/matcher/v3/matcher.pb.h" @@ -19,7 +20,6 @@ #include "source/common/matcher/value_input_matcher.h" #include "absl/strings/string_view.h" -#include "absl/types/optional.h" namespace Envoy { namespace Matcher { @@ -52,14 +52,14 @@ template using FieldMatcherFactoryCb = std::function class AnyMatcher : public MatchTree { public: - explicit AnyMatcher(absl::optional> on_no_match) + explicit AnyMatcher(std::optional> on_no_match) : on_no_match_(std::move(on_no_match)) {} ActionMatchResult match(const DataType& data, SkippedMatchCb skipped_match_cb = nullptr) override { return MatchTree::handleRecursionAndSkips(on_no_match_, data, skipped_match_cb); } - const absl::optional> on_no_match_; + const std::optional> on_no_match_; }; /** @@ -149,12 +149,12 @@ class MatchTreeFactory : public OnMatchFactory { PANIC_DUE_TO_CORRUPT_ENUM; } - absl::optional> + std::optional> createOnMatch(const xds::type::matcher::v3::Matcher::OnMatch& on_match) override { return createOnMatchBase(on_match); } - absl::optional> + std::optional> createOnMatch(const envoy::config::common::matcher::v3::Matcher::OnMatch& on_match) override { return createOnMatchBase(on_match); } @@ -166,7 +166,7 @@ class MatchTreeFactory : public OnMatchFactory { return [on_no_match]() { return std::make_unique>( - on_no_match ? absl::make_optional((*on_no_match)()) : absl::nullopt); + on_no_match ? std::make_optional((*on_no_match)()) : std::nullopt); }; } template @@ -183,7 +183,7 @@ class MatchTreeFactory : public OnMatchFactory { auto on_no_match = createOnMatch(config.on_no_match()); return [matcher_factories, on_no_match]() { auto list_matcher = std::make_unique>( - on_no_match ? absl::make_optional((*on_no_match)()) : absl::nullopt); + on_no_match ? std::make_optional((*on_no_match)()) : std::nullopt); for (const auto& matcher : matcher_factories) { list_matcher->addMatcher(matcher.first(), matcher.second()); @@ -276,12 +276,12 @@ class MatchTreeFactory : public OnMatchFactory { } using MapCreationFunction = std::function>>( - DataInputPtr&& data_input, absl::optional> on_no_match)>; + DataInputPtr&& data_input, std::optional> on_no_match)>; template