Compare commits

...
65 Commits
Author SHA1 Message Date
Ara Sadoyan 56c41e2c19 Cargo.toml 2026-10-21 19:13:18 +02:00
Ara Sadoyan 6ea8b42835 Improved API for /conf endpoint 2026-10-21 19:05:21 +02:00
Ara Sadoyan af771287cb README update 2026-10-20 17:43:38 +02:00
Ara Sadoyan 0c489b649a Improved error handling, anti unwrap()-ing 2026-10-20 17:06:55 +02:00
Ara Sadoyan f895ba4554 Restructured cache. Added Redis|Valkey|Dragonfly support. 2026-10-19 19:13:24 +02:00
Ara Sadoyan a2b14b0dea Cargo version update 2026-10-16 19:06:33 +02:00
Ara Sadoyan 0ee39559ca Master, multi slave cluster 2026-10-16 17:57:33 +02:00
Ara Sadoyan cdeb120a39 ACME DNS DigitalOcean 2026-10-16 17:56:57 +02:00
Ara Sadoyan ee578854e3 Fix scheme detection on HC disable upstreams 2026-10-07 15:02:51 +02:00
Ara Sadoyan a8dc98f636 cargo version 2026-10-05 19:07:32 +02:00
Ara Sadoyan dad9539b9c Fix provider separation 2026-10-05 19:06:44 +02:00
Ara Sadoyan ab5252ee52 Fixed bug with HTTP-01 Challenge 2026-10-05 18:41:39 +02:00
Ara Sadoyan 0f7f7b723b Added Route53 support. 2026-10-05 15:40:07 +02:00
Ara Sadoyan 2b0f4d68e6 Cargo version fix 2026-10-04 12:11:36 +02:00
Ara Sadoyan 7b248f2190 Polished lookup wait for DNS-01 challenge 2026-10-04 12:08:10 +02:00
Ara Sadoyan 1ecf3a69ff Completed skeleton for DNS challenge, added Cloudflare DNS integration. 2026-10-03 13:49:07 +02:00
Ara Sadoyan 3bc398610b Intermediate commit fo DNS Challenge 2026-10-03 10:56:47 +02:00
Ara Sadoyan 91d2d414a3 Minor improvements, type changes 2026-10-01 15:19:18 +02:00
Ara Sadoyan ea09efed4d Merge branch 'main' of github.com:sadoyan/aralez 2026-09-28 11:51:23 +02:00
Ara Sadoyan 2d19415e92 merge 2026-09-28 11:51:01 +02:00
Ara Sadoyan 9aa72d20dc Merge pull request #59 from sergelogvinov/linter
fix linter warnings
2026-09-28 11:49:35 +02:00
Serge Logvinov 0814d28df6 fix linter warnings 2026-09-28 07:15:07 +00:00
Ara Sadoyan e7b4f9a327 Merge pull request #58 from sergelogvinov/build
add compat versions
2026-09-27 10:50:33 +02:00
Ara Sadoyan 1fab52456f Optimize structured logging. 2026-09-26 18:49:37 +02:00
Serge Logvinov c4732cafd2 feat: add compat verions 2026-09-26 16:04:10 +02:00
Ara Sadoyan d623a2da72 Optimize structured logging. 2026-09-26 15:58:46 +02:00
Ara Sadoyan a145c5e71b file delete 2026-09-25 18:07:03 +02:00
Ara Sadoyan 9de8c9ba97 version change 2026-09-25 18:01:15 +02:00
Ara Sadoyan 502c2ed4c2 Merge pull request #57 from sergelogvinov/build
feat: github actions
2026-09-25 17:54:45 +02:00
Ara Sadoyan a8b3f804f4 Done with logging to ElasticSearch. 2026-09-25 17:52:01 +02:00
Ara Sadoyan 1b733bc879 Intermediate ElasticSearch commit . 2026-09-25 15:26:31 +02:00
Ara Sadoyan 92ed7cebde Intermediate ElasticSearch commit . 2026-09-25 12:55:38 +02:00
Ara Sadoyan 3fe037de5a Making custom Elsticsearch logger 2. 2026-09-24 18:27:53 +02:00
Ara Sadoyan d56968c4c3 Making custom Elsticsearch logger. 2026-09-23 17:57:18 +02:00
Ara Sadoyan 71ebc6376e improving logging 2026-09-23 15:28:41 +02:00
Ara Sadoyan 54ac35a4c9 added inventory for immutable core logging files 2026-09-22 18:48:42 +02:00
Ara Sadoyan 639365c43b restructurization of logging subsystem 2026-09-22 18:25:52 +02:00
Ara Sadoyan 4cd9b459b0 Code restructurization 2026-09-21 15:31:10 +02:00
Ara Sadoyan 4ae92a264b Structural changes, move auth to separate module . 2026-09-20 18:57:45 +02:00
Serge Logvinov efcf83033f feat: github actions 2026-09-19 12:41:38 +02:00
Ara Sadoyan 5fdfa17265 Update to Pingora 0.9, improved logging, Fix cache key bug. 2026-09-18 18:33:35 +02:00
Ara Sadoyan 5efcf07dd0 Updated example main.yaml 2026-09-17 11:35:52 +02:00
Ara Sadoyan 02a329e66b Added log pattern to main.cfg 2026-09-16 19:18:18 +02:00
Ara Sadoyan 444579eefe Minor changes 2026-09-14 10:55:44 +02:00
Ara Sadoyan 0831ffb801 Merge pull request #53 from sergelogvinov/chart
feat: add aralez helm chart
2026-09-09 19:08:06 +02:00
Ara Sadoyan 5d55e2b120 configs 2026-09-09 19:01:38 +02:00
Ara Sadoyan b3c614e592 configs 2026-09-09 18:54:25 +02:00
Ara Sadoyan b22bb2fe16 Config files update, cleanup 2026-09-09 18:52:32 +02:00
Ara Sadoyan e6204a2b1d version change 2026-09-09 18:43:49 +02:00
Ara Sadoyan 6a78acd486 version change 2026-09-09 18:43:28 +02:00
Ara Sadoyan 1a2485b87a Implemented EndpointSlices #55 2026-09-09 18:42:41 +02:00
Ara Sadoyan 3329c34a09 working on #55 2026-09-09 15:39:28 +02:00
Serge 6df63f69cd feat: add aralez helm chart
Add basic Aralez deployment configuration, Helm chart linting workflow,
and Dependabot configuration.
2026-09-06 11:33:16 +02:00
Ara Sadoyan 4a4f3e6187 fixed closed #52 2026-09-04 16:24:49 +02:00
Ara Sadoyan 31f513cc2a hcl examples 2026-09-01 12:53:42 +02:00
Ara Sadoyan 0006d27d02 hcl examples 2026-09-01 12:19:09 +02:00
Ara Sadoyan d2d4100cba Finalized Consul integration,
Cached headers bug fix
2026-08-31 19:18:10 +02:00
Ara Sadoyan f575007044 Rewrite of Consul integration. 2026-08-30 18:38:31 +02:00
Ara Sadoyan bb43df0e4d Full rewrite of Kubernetes integration. 2026-08-25 19:32:42 +02:00
Ara Sadoyan 29c3d6a413 Full rewrite of Kubernetes integration. 2026-08-25 19:31:05 +02:00
Ara Sadoyan c21374f2a2 Full rewrite of Kubernetes integration. 2026-08-25 19:29:53 +02:00
Ara Sadoyan 51eec524ca Cargo 2026-08-10 15:40:50 +02:00
Ara Sadoyan 97ac839001 Removed reqwest dependency, moved all http client reqvetsts to native pingora. 2026-08-06 19:38:03 +02:00
Ara Sadoyan b63d822577 Rewrote and optimized healtchecks 2026-08-05 18:51:07 +02:00
Ara Sadoyan 600cb602e3 Readme update 2026-07-28 14:45:46 +02:00
101 changed files with 7114 additions and 1784 deletions
+9
View File
@@ -0,0 +1,9 @@
.git/
.github/
.gitignore
.dockerignore
/charts/
#
docker-compose.yml
#
/.cargo/
+9
View File
@@ -0,0 +1,9 @@
helm-extra-args: --timeout 300s
check-version-increment: true
debug: true
chart-dirs:
- charts
validate-maintainers: true
namespace: default
release-label: test
target-branch: main
+23
View File
@@ -0,0 +1,23 @@
---
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
commit-message:
prefix: "chore:"
open-pull-requests-limit: 8
rebase-strategy: disabled
schedule:
interval: "monthly"
day: "monday"
time: "10:00"
timezone: "UTC"
cooldown:
default-days: 7
groups:
actions:
patterns:
- "actions/*"
- "helm/*"
+50
View File
@@ -0,0 +1,50 @@
name: Build check
on:
workflow_dispatch: {}
pull_request:
branches:
- main
paths:
- "src/**"
- "Dockerfile"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build and Test
timeout-minutes: 15
runs-on: ubuntu-24.04
if: github.event.pull_request.draft == false
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Install cargo-deny
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
with:
tool: cargo-deny
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Rust build
run: make build test
- name: Rust fmt
run: make fmt
- name: Rust linter
run: make lint
- name: Rust dependencies check
run: make deps
+28
View File
@@ -0,0 +1,28 @@
name: Helm chart check
on:
pull_request:
branches:
- main
paths:
- "charts/**"
jobs:
helm-lint:
name: Helm chart check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install chart-testing tools
id: lint
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
- name: Run helm chart linter
run: ct --config .github/ct.yml lint
- name: Run helm template
run: helm lint charts/aralez
+58
View File
@@ -0,0 +1,58 @@
name: Release
on:
workflow_dispatch: {}
# push:
# tags:
# - "v*"
jobs:
goreleaser:
name: GoReleaser
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up docker buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Github registry login
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
timeout-minutes: 60
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "v2.18.2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ github.repository_owner }}
+1
View File
@@ -21,6 +21,7 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
/target /target
/.zig-cache/
/z_shpo /z_shpo
/configs /configs
Makefile Makefile
+126
View File
@@ -0,0 +1,126 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: aralez
before:
hooks:
- cargo install --locked cargo-zigbuild
dist: target
builds:
- id: aralez
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-musl
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-compat
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
env:
- RUSTFLAGS=-C target-cpu=x86-64
- id: aralez-compat-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
env:
- RUSTFLAGS=-C target-cpu=x86-64
snapshot:
version_template: edge
archives:
- id: aralez
ids:
- aralez
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-glibc"
- id: aralez-musl
ids:
- aralez-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-musl"
- id: aralez-compat
ids:
- aralez-compat
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-glibc"
- id: aralez-compat-musl
ids:
- aralez-compat-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-musl"
dockers_v2:
- id: aralez
ids:
- aralez
flags:
- --target=release
images:
- "ghcr.io/{{ .Env.USERNAME }}/{{ .ProjectName }}"
# - "{{ .Env.USERNAME }}/{{ .ProjectName }}"
tags:
- "{{ if not .IsSnapshot }}v{{.Version}}{{ else }}edge{{ end }}"
platforms:
- linux/amd64
- linux/arm64
annotations:
org.opencontainers.image.name: "{{ .ProjectName }}"
org.opencontainers.image.licenses: "Apache-2.0"
org.opencontainers.image.source: "https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}"
org.opencontainers.image.url: "{{ .GitURL }}"
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
io.artifacthub.package.readme-url: "https://raw.githubusercontent.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/main/README.md"
io.artifacthub.package.license: "Apache-2.0"
docker_signs:
- id: docker
ids:
- aralez
cmd: cosign
args:
- sign
- "--yes"
- "${artifact}"
output: true
checksum:
name_template: checksums.txt
algorithm: sha256
signs:
- artifacts: checksum
signature: "${artifact}.sigstore.json"
cmd: cosign
args:
- sign-blob
- "--yes"
- "--bundle=${signature}"
- "${artifact}"
output: true
Generated
+2010 -560
View File
File diff suppressed because it is too large Load Diff
+37 -23
View File
@@ -1,7 +1,7 @@
[package] [package]
name = "aralez" name = "aralez"
version = "0.92.16" version = "0.95.2"
edition = "2021" edition = "2024"
license = "Apache-2.0" license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora" description = "Reverse proxy built on top of Cloudflare's Pingora"
exclude = ["configs/*"] exclude = ["configs/*"]
@@ -15,47 +15,61 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.9.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.8.1" pingora-core = "0.9.0"
pingora-proxy = "0.8.1" pingora-proxy = "0.9.0"
pingora-http = "0.8.1" pingora-http = "0.9.0"
pingora-limits = "0.8.1" pingora-limits = "0.9.0"
pingora-cache = "0.8.1" pingora-cache = "0.9.0"
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
async-trait = "0.1.89" async-trait = "0.1.92"
log = "0.4.33" log = "0.4.34"
notify = "9.0.0-rc.4" notify = "9.0.0-rc.4"
axum = { version = "0.8.9" } axum = { version = "0.8.9" }
reqwest = { version = "0.13.4", features = ["json", "stream", "blocking"] } serde = { version = "1.0.229", features = ["derive", "rc"] }
serde = { version = "1.0.228", features = ["derive"] } #serde = { version = "1.0.229", features = ["derive"] }
noyalib = { version = "0.0.15", features = ["compat-serde-yaml"] } noyalib = { version = "0.0.44", features = ["compat-serde-yaml"] }
rand = "0.10.2" rand = "0.10.2"
base64 = "0.22.1" base64 = "0.23.1"
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] } jsonwebtoken = { version = "11.1.0", default-features = false, features = ["use_pem", "rust_crypto"] }
sha2 = { version = "0.11.0-rc.5", default-features = false } sha2 = { version = "0.11.0", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
arc-swap = "1.9.2" arc-swap = "1.9.2"
prometheus = "0.14.0" prometheus = "0.14.0"
x509-parser = "0.18.1" x509-parser = "0.18.1"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.7.0", features = ["fs"] } tower-http = { version = "0.7.1", features = ["fs"] }
privdrop = "0.5.6" privdrop = "0.5.6"
serde_json = "1.0.150" serde_json = "1.0.151"
subtle = "2.6.1" subtle = "2.6.1"
moka = { version = "0.12.15", features = ["sync"] } moka = { version = "0.12.16", features = ["sync"] }
ahash = "0.8.12" ahash = "0.8.12"
instant-acme = "0.8.5" instant-acme = "0.8.5"
rcgen = "0.14.8" rcgen = "0.14.10"
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] } log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] }
mimalloc = { version = "0.1.52", default-features = false } mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4" signal-hook = "0.4.4"
sd-notify = "0.5.0" sd-notify = "0.5.0"
libc = "0.2.186" libc = "0.2.186"
tokio-native-tls = "0.3.1" tokio-native-tls = "0.3.1"
hyper = "1.10.1" hyper = "1.11.1"
hyper-util = "0.1.20" hyper-util = "0.1.20"
http-body-util = "0.1.3" http-body-util = "0.1.5"
bytes = "1.12.1" bytes = "1.12.1"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
kube = { version = "4.2.0", features = ["runtime", "client", "derive"] }
k8s-openapi = { version = "0.28.0", features = ["v1_32"] }
futures = "0.3.34"
lazy_static = "1.5.0"
anyhow = "1.0.103"
inventory = "0.3.24"
elasticsearch = "9.1.0-alpha.1"
humantime = "2.4.0"
reqwest = "0.12.28"
hickory-resolver = { version = "0.26", features = ["tokio"] }
aws-sdk-route53 = "1.127.0"
aws-config = { version = "1.12.0", features = ["behavior-version-latest"] }
redis = { version = "1.7.1", features = ["tokio-comp", "connection-manager"] }
+49
View File
@@ -0,0 +1,49 @@
# syntax = docker/dockerfile:1.22
########################################
FROM rust:1.98-trixie AS builder
ARG TARGETARCH
RUN apt-get update && \
apt-get install -y build-essential make cmake pkg-config libssl-dev musl-dev musl-tools
RUN case "${TARGETARCH}" in \
amd64) \
apt-get install -y --no-install-recommends gcc-x86-64-linux-gnu && \
rustup target add x86_64-unknown-linux-gnu; \
rustup target add x86_64-unknown-linux-musl; \
;; \
arm64) \
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu && \
rustup target add aarch64-unknown-linux-gnu; \
rustup target add aarch64-unknown-linux-musl; \
;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac
RUN rustup component add rustfmt clippy && \
cargo install cargo-deny
RUN ARCH=$(case "${TARGETARCH}" in \
amd64) echo "x86_64" ;; \
arm64) echo "aarch64" ;; \
*) echo "${TARGETARCH}" ;; \
esac) && curl -LO https://ziglang.org/download/0.16.0/zig-${ARCH}-linux-0.16.0.tar.xz \
&& tar -xf zig-${ARCH}-linux-0.16.0.tar.xz -C /opt \
&& ln -s /opt/zig-${ARCH}-linux-0.16.0/zig /usr/local/bin/zig
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list && \
apt-get update && apt-get install -y goreleaser
RUN cargo install cargo-binstall --locked && \
cargo binstall cargo-zigbuild --no-confirm
COPY [".", "/src"]
########################################
FROM --platform=${TARGETARCH} gcr.io/distroless/cc-debian13:nonroot AS release
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/aralez /usr/local/bin/aralez
ENTRYPOINT ["/usr/local/bin/aralez"]
+68
View File
@@ -0,0 +1,68 @@
########################################################
USERNAME ?= aralez
TARGET ?= --all-targets
FEATURES ?= --all-features
########################################################
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
########################################################
.PHONY: all
all: build test fmt checkup ## Build and test project
.PHONY: build
build: ## Build project
cargo build --workspace $(TARGET) $(FEATURES)
.PHONY: test
test: ## Run tests
cargo test --workspace $(TARGET) $(FEATURES)
.PHONY: fmt
fmt: ## Check code formatting
cargo fmt --all -- --check
.PHONY: lint
lint: ## Run linter
cargo clippy --workspace $(TARGET) $(FEATURES) -- -D warnings
.PHONY: checkup
checkup: lint ## Run linter and check project
cargo check --workspace $(TARGET) $(FEATURES)
.PHONY: deps
deps: ## Check project dependencies for advisories
cargo deny check advisories
########################################################
.PHONY: update
update:
cargo update --verbose
.PHONY: features
features:
cargo features
########################################################
.PHONY: fix
fix:
cargo fix
.PHONY: fix-all
fix-all:
cargo fix --all
cargo clippy --workspace $(TARGET) $(FEATURES) --fix
########################################################
.PHONY: docker-build
docker-build: ## Build project in Docker
docker build --target=builder -t builder .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}:/src -e USERNAME=${USERNAME} -w /src -ti builder:latest
+79 -54
View File
@@ -6,13 +6,12 @@
### **Reverse proxy built on top of Cloudflare's Pingora** ### **Reverse proxy built on top of Cloudflare's Pingora**
Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a
featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a lightweight ingress-style proxy. lightweight ingress-style proxy.
--- ---
What Aralez means ? What Aralez means ? **Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
**Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box. Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
@@ -36,6 +35,7 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers
- **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge. - **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge.
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection. - **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **Built in rate limiter** — Globar or route limit requests to upstreams. - **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Caching Proxy** — In memory cache with configurable TTL, without external dependencies.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification. - **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth** - **Basic Auth**
- **API Key** via `x-api-key` header - **API Key** via `x-api-key` header
@@ -60,37 +60,47 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers
### `main.yaml` ### `main.yaml`
| Key | Example Value | Description | | Key | Example Value | Description |
|----------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 | | **threads** | `12` | Number of running daemon threads. Optional, defaults to 1 |
| **runuser** | aralez | Optional. Username for running aralez after dropping root privileges (requires launch as root) | | **runuser** | `aralez` | Optional. Username for running aralez after dropping root privileges (requires launch as root) |
| **rungroup** | aralez | Optional. Group for running aralez after dropping root privileges (requires launch as root) | | **rungroup** | `aralez` | Optional. Group for running aralez after dropping root privileges (requires launch as root) |
| **daemon** | false | Run in background (boolean) | | **daemon** | `false` | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections | | **upstream_keepalive_pool_size** | `500` | Pool size for upstream keepalive connections |
| **pid_file** | /tmp/aralez.pid | Path to PID file | | **pid_file** | `/tmp/aralez.pid` | Path to PID file |
| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | | **upgrade_sock** | `/tmp/aralez.sock` | Path to socket file |
| **proxy_tls_grade** | high, medium, unsafe | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) | | **config_address** | `0.0.0.0:3000` | HTTP API address for pushing upstreams.yaml from remote location |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address | | **proxy_tls_grade** | `high, medium, unsafe` | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) | | **proxy_address_http** | `0.0.0.0:6193` | Aralez HTTP bind address |
| **proxy_configs** | /etc/aralez/ | Direcotry containing configuration files, must be writeable by user `aralez` | | **proxy_address_tls** | `0.0.0.0:6194` | Aralez HTTPS bind address (Optional) |
| **upstreams_conf** | /etc/aralez/upstreams.yaml | Location of the upstreams file | | **proxy_configs** | `/etc/aralez/` | Direcotry containing configuration files, must be writeable by user `aralez` |
| **access_log** | access | Configure access logging. Values: `access, error` | | **upstreams_conf** | `/etc/aralez/upstreams.yaml` | Location of the upstreams file |
| **log_level** | info | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` | | **access_log** | `access` | Configure access logging. Values: `access, error` |
| **log_file** | /tmp/aralez.log, 20, 5, compress | Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. Defaults to None, 100, 5, No | | **log_level** | `info` | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` |
| **hc_method** | HEAD | Healthcheck method: HEAD, GET, POST (UPPERCASE) | | **log_file** | `/tmp/aralez.log, 20, 5, compress` | Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. Defaults to None, 100, 5, No |
| **hc_interval** | 2 | Interval for health checks in seconds | | **log_pattern** | `{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}` | Optional log pattern, defaults to `{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}`. See log4rs official [documentation](https://docs.rs/log4rs/latest/log4rs/encode/pattern/struct.PatternEncoderDeserializer.html#configuration) |
| **file_server_folder** | /some/local/folder | Optional. Local folder to serve | | **hc_method** | `HEAD` | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **file_server_address** | 127.0.0.1:3002 | Optional. Local address for file server | | **hc_interval** | `2` | Interval for health checks in seconds |
| **config_api_enabled** | true | Enable/disable remote config push capability | | **file_server_folder** | `/some/local/folder` | Optional. Local folder to serve |
| **cache_size_mb** | 50 | Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out. | | **file_server_address** | `127.0.0.1:3002` | Optional. Local address for file server |
| **cache_ttl** | 10 | Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set. | | **config_api_enabled** | `true` | Enable/disable remote config push capability |
| **cache_size_mb** | `50` | Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out. |
| **cache_enabled** | `bool` | Optional. Disabled by default |
| **cache_storage** | `memory, redis://host:port` | Optional Defaults to memory works with Redis, Valkey, Dragonfly (requires redis:// prefix) |
| **cache_ttl** | `60` | Optional. Set internal cache with size in megabytes of value. Defaults to 50Mb if cache_enabled: true |
| **tcp_keepalive_idle** | `60` | Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client. Defaults to 60 |
| **tcp_keepalive_interval** | `10` | Seconds between individual keepalive probes if the client does not respond. Defaults to 10 |
| **tcp_keepalive_count** | `5` | Number of unanswered probes before the kernel declares the connection dead and closes it. Defaults to 5 |
| **cluster_member** | `master, worker` | Optional. Enables cluster mode, Sets node type to master or worker. Disabled by default |
| **worker_nodes** | `http(s)://host:port,http(s)://host:port` | Required is if cluster_member: master. Comma separated list of worker nodes http(s)://host:port |
| **log_structured** | `elasticsearch` | Optional. Enables structured logging and sets backend type. Only elasticsearch et the moment. |
| **acme_dns_provider** | `cloudflare, route53, digitalocean, etc` | Optional. Enable ACME DNS-01 challenge, sets DNS provider. ONly coudflare, route53, digitalocean at the moment. |
--- ---
## Installation ## Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Make the binary executable `chmod 755 ./aralez-VERSION` and run.
Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
@@ -108,8 +118,7 @@ File names:
**glibc** builds are in general faster, but have few, basic, Glibc dependencies: **glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies. **musl** builds are 100% portable, static compiled binaries and have zero system dependencies. In general musl builds have a little less performance.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones. The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
@@ -299,11 +308,27 @@ openssl req -x509 -newkey rsa:4096 \
## Remote Config API ## Remote Config API
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. Push new `upstreams.yaml` over HTTP to `config_address` (`127.0.0.1:3000` by default).
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml` Useful for CI/CD automation or remote config updates.
```bash ```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY} curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf
```
Don't run API on address 0.0.0.0 on production servers, keep it on 127.0.0.1 and configure appropriate upstream with supported authentication and TLS termination
Example:
```yaml
config.example.com:
paths:
"/conf":
authorization:
type: "basic"
data: "user:pass"
healthcheck: false
servers:
- "127.0.0.1:3000"
``` ```
--- ---
@@ -381,22 +406,22 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** | | Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:| |--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot | | **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? | | **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes | | **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual | | **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | | **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual | | **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | | **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes | | **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes | | **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | | **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No | | **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | Rust | C | C | Go | Go | C++ | | **Built With** | Rust | C | C | Go | Go | C++ |
--- ---
@@ -443,7 +468,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
## Results reflect synthetic performance under optimal conditions. ## Results reflect synthetic performance under optimal conditions.
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
- Duration : 10 Minutes - Duration : 10 Minutes
- Binary : aralez-x86_64-glibc - Binary : aralez-x86_64-glibc
@@ -499,7 +524,7 @@ Error distribution:
![Aralez](https://netangels.net/utils/glibc10.png) ![Aralez](https://netangels.net/utils/glibc10.png)
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
- Duration : 10 Minutes - Duration : 10 Minutes
- Binary : aralez-x86_64-musl - Binary : aralez-x86_64-musl
@@ -557,7 +582,7 @@ Error distribution:
## Aralez, Nginx, Traefik performance benchmark ## Aralez, Nginx, Traefik performance benchmark
This benchmark is done on 4 servers. With CPU Intel(R) Xeon(R) E-2174G CPU @ 3.80GHz, 64 GB RAM. This benchmark is done on 4 servers. With CPU Intel (R) Xeon (R) E-2174G CPU @ 3.80GHz, 64 GB RAM.
1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could . 1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could .
2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance. 2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance.
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+18
View File
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://json.schemastore.org/chart.json
apiVersion: v2
name: aralez
description: Ingress controller for Kubernetes
type: application
home: https://github.com/sadoyan/aralez/charts
sources:
- https://github.com/sadoyan/helm-charts/tree/main/charts/aralez
keywords:
- ingress-controller
- aralez
maintainers: []
#
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
#
# renovate: datasource=docker depName=ghcr.io/sadoyan/aralez
appVersion: 0.93.3
+17
View File
@@ -0,0 +1,17 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
+18
View File
@@ -0,0 +1,18 @@
ingressClass: aralez
ingressClassResource:
enabled: true
# hostNetwork: true
# hostPort:
# ports:
# http: 8080
# https: 8443
priorityClassName: system-cluster-critical
env: []
args: []
useDaemonSet: true
terminationGracePeriodSeconds: 120
View File
+104
View File
@@ -0,0 +1,104 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "aralez.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "aralez.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "aralez.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "aralez.labels" -}}
helm.sh/chart: {{ include "aralez.chart" . }}
{{ include "aralez.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.podLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "aralez.selectorLabels" -}}
app.kubernetes.io/name: {{ include "aralez.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Return a podAffinity/podAntiAffinity definition
*/}}
{{- define "affinities.pods" -}}
{{- if eq .Values.podAntiAffinityPreset "soft" }}
{{- include "affinities.pods.soft" . -}}
{{- else if eq .Values.podAntiAffinityPreset "hard" }}
{{- include "affinities.pods.hard" . -}}
{{- end -}}
{{- end -}}
{{/*
Return a soft podAntiAffinity definition
*/}}
{{- define "affinities.pods.soft" -}}
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 12 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
weight: 1
{{- end -}}
{{/*
Return a hard podAntiAffinity definition
*/}}
{{- define "affinities.pods.hard" -}}
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 10 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "aralez.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "aralez.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- $ca := genCA .Chart.Name 3650 }}
{{- $server := genSignedCert "server" (list "127.0.0.1") nil 3650 $ca }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "aralez.fullname" . }}-tls
labels:
{{- include "aralez.labels" . | nindent 4 }}
type: kubernetes.io/tls
data:
{{- $secretName := printf "%s-tls" (include "aralez.fullname" .) }}
{{- $secretData := (lookup "v1" "Secret" .Release.Namespace $secretName).data }}
{{- if and $secretData (hasKey $secretData "ca.crt") }}
ca.crt: {{ get $secretData "ca.crt" }}
tls.crt: {{ get $secretData "tls.crt" }}
tls.key: {{ get $secretData "tls.key" }}
{{- else }}
ca.crt: {{ $ca.Cert | b64enc }}
tls.crt: {{ $server.Cert | b64enc }}
tls.key: {{ $server.Key | b64enc }}
{{- end }}
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
data:
aralez.yaml: |
daemon: false
upstreams_conf: /etc/aralez/upstreams.yaml
proxy_configs: /etc/aralez-proxy
proxy_address_http: "0.0.0.0:{{ .Values.hostPort.ports.http }}"
proxy_address_tls: "0.0.0.0:{{ .Values.hostPort.ports.https }}"
config_api_enabled: false
config_address: "0.0.0.0:3000"
upgrade_sock: /tmp/aralez.sock
pid_file: /tmp/aralez.pid
log_level: debug
hc_method: HEAD
hc_interval: 5
upstreams.yaml: |
provider: "kubernetes"
sticky_sessions: 172000
kubernetes: {}
{{- with .Values.upstreams }}
{{- toYaml . | nindent 4 }}
{{- end }}
+130
View File
@@ -0,0 +1,130 @@
apiVersion: apps/v1
{{- if .Values.useDaemonSet }}
kind: DaemonSet
{{- else }}
kind: Deployment
{{- end }}
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
{{- if not (or .Values.autoscaling.enabled .Values.useDaemonSet) }}
replicas: {{ .Values.replicaCount }}
{{- end }}
{{- if not .Values.useDaemonSet }}
strategy:
type: {{ .Values.updateStrategy.type }}
{{- else }}
updateStrategy:
type: {{ .Values.updateStrategy.type }}
{{- end }}
selector:
matchLabels:
{{- include "aralez.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "aralez.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.upstreams }}
checksum/upstreams: {{ toJson .Values.upstreams | sha256sum }}
{{- end }}
spec:
enableServiceLinks: false
{{- if and .Values.hostNetwork .Values.useDaemonSet }}
hostNetwork: {{ .Values.hostNetwork }}
dnsPolicy: "ClusterFirstWithHostNet"
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "aralez.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["aralez"]
args:
- --conf=/etc/aralez/aralez.yaml
{{- range $value := .Values.args }}
- {{ $value }}
{{- end }}
env:
{{- with .Values.env }}
{{- . | toYaml | nindent 12 }}
{{- end }}
ports:
{{- if .Values.hostPort.ports.http }}
- name: http
containerPort: {{ .Values.hostPort.ports.http }}
protocol: TCP
{{- end }}
{{- if .Values.hostPort.ports.https }}
- name: https
containerPort: {{ .Values.hostPort.ports.https }}
protocol: TCP
{{- end }}
# readinessProbe:
# httpGet:
# path: /status
# port: 3000
# initialDelaySeconds: 30
# timeoutSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/aralez
- name: tlscerts
mountPath: /etc/aralez-proxy/certificates
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "aralez.fullname" . }}
- name: tlscerts
secret:
secretName: {{ include "aralez.fullname" . }}-tls
defaultMode: 256
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
affinity:
{{- include "affinities.pods" . | nindent 8 }}
{{- with .Values.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- if and .Values.autoscaling.enabled (not .Values.useDaemonSet) }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "aralez.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}
+10
View File
@@ -0,0 +1,10 @@
{{- if .Values.ingressClassResource.enabled -}}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
{{- include "aralez.labels" . | nindent 4 }}
name: {{ .Values.ingressClass }}
spec:
controller: {{ .Values.ingressClassResource.controllerValue }}
{{- end }}
+41
View File
@@ -0,0 +1,41 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "NodePort" }}
externalTrafficPolicy: Local
{{- end }}
{{- with .Values.service.ipFamilies }}
ipFamilies:
{{- toYaml . | nindent 4 }}
{{- end }}
ipFamilyPolicy: {{ if eq (len .Values.service.ipFamilies) 2 }}PreferDualStack{{ else }}SingleStack{{ end }}
ports:
{{- if .Values.hostPort.ports.http }}
- port: 80
targetPort: http
protocol: TCP
name: http
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.http }}
{{- end }}
{{- end }}
{{- if .Values.hostPort.ports.https }}
- port: 443
targetPort: https
protocol: TCP
name: https
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.https }}
{{- end }}
{{- end }}
selector:
{{- include "aralez.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,52 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "aralez.serviceAccountName" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "aralez.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "aralez.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
+164
View File
@@ -0,0 +1,164 @@
# Default values for aralez.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/sadoyan/aralez
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Environment variables
env: []
# -- Aralez arguments
args: []
# -- Aralez upstreams file body
# refs: https://aralez.rs/docs/kubernetes/
upstreams:
rate_limit: 500000
x4xx_limit: 100000
to_https: true
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- Annotations for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
# -- Extra labels for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
# -- Pod Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
podSecurityContext:
runAsNonRoot: true
runAsUser: 9999
runAsGroup: 9999
fsGroup: 9999
fsGroupChangePolicy: "OnRootMismatch"
# -- Container Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
securityContext:
seccompProfile:
type: RuntimeDefault
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# -- Pod host network
# Use it only for performance reason
hostNetwork: false
# -- Service node port
# refs: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
hostPort:
enabled: false
ports:
http: 8080
https: 8443
ingressClass: aralez
ingressClassResource:
enabled: true
controllerValue: aralez.proxy/ingress-controller
# -- Bitwarden service parameters
# ref: https://kubernetes.io/docs/user-guide/services/
service:
# -- service type
type: ClusterIP
annotations: {}
# -- IP families for service
# possible values: IPv4, IPv6
# ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/
ipFamilies: [IPv4]
# -- Resource requests and limits.
# ref: https://kubernetes.io/docs/user-guide/compute-resources/
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# -- Horizontal and Vertical pod autoscaler.
# ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# -- Use a daemonset instead of a deployment
useDaemonSet: false
# -- The time in seconds that is allowed for a Aralez instance to gracefully shutdown
# ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks
terminationGracePeriodSeconds: 120
## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
# -- Priority Class Name
# ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
priorityClassName: ""
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
# -- Node labels for pod assignment.
# ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}
# -- Tolerations for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []
# -- Pod Anti Affinity
# soft/hard
podAntiAffinityPreset: soft
# -- Affinity for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
affinity: {}
+66
View File
@@ -0,0 +1,66 @@
job "apache" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Apache" {
network {
port "apache" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "health" {
service {
port = "apache"
tags = [
"aralez.service=yes",
"aralez.host=apache.example.com",
"aralez.path=/",
"aralez.redirect=http://checkip.amazonaws.com"
]
check {
type = "http"
port = "apache"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "httpd:latest"
force_pull = true
ports = ["apache"]
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
job "echo" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Echo" {
network {
port "echo" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "echo"
tags = [
"aralez.service=yes",
"aralez.host=echo.example.com",
"aralez.path=/",
"aralez.rate=20",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Client-Header:Some simple header",
"aralez.server_header=X-Server-Header:Some simple header",
]
check {
type = "http"
port = "echo"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "ealen/echo-server:latest"
force_pull = true
ports = ["echo"]
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
job "nginx" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "NginX" {
network {
port "nginx" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "nginx"
tags = [
"aralez.service=yes",
"aralez.host=nginx.example.com",
"aralez.path=/",
"aralez.rate=1",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Some:Some Random Header",
"aralez.client_header=X-Emos:Redaeh Modnar Emos",
"aralez.server_header=X-From:Aralez & Consul",
"aralez.server_header=X-Mrof: Lusnoc & Zelara",
]
check {
type = "http"
port = "nginx"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "nginx:latest"
force_pull = true
ports = ["nginx"]
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# 1. Define the IngressClass so Kubernetes knows "aralez" is a valid controller
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: aralez
namespace: upstreams
spec:
controller: aralez.proxy/ingress-controller
+76
View File
@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
namespace: aralez
labels:
app.kubernetes.io/name: aralez
app.kubernetes.io/component: ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
#image: sadoyan/aralez:latest
image: nginx:latest
imagePullPolicy: IfNotPresent
env:
- name: ARALEZ_SERVICE_NAME
value: "aralez-service"
- name: ARALEZ_SERVICE_NAMESPACE
value: "aralez"
- name: ARALEZ_EXTERNAL_IP # Only for bare metal deployment, remove this in Cloud.
value: "192.168.10.10" # Only for bare metal deployment, remove this in Cloud.
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
volumeMounts:
- name: main-config
mountPath: /etc/aralez/main.yaml
subPath: main.yaml
readOnly: true
- name: upstreams-config
mountPath: /etc/aralez/upstreams.yml
subPath: upstreams.yml
readOnly: true
- name: tls-certs
mountPath: /etc/aralez/certificates
readOnly: true
volumes:
- name: main-config
configMap:
name: aralez-main-config
- name: upstreams-config
configMap:
name: aralez-upstreams-config
- name: tls-certs
secret:
secretName: aralez-tls
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: aralez-service
namespace: aralez
spec:
type: NodePort
selector:
app: aralez
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
namespace: aralez
type: kubernetes.io/tls
data:
tls1.crt: "<FIRST_BASE64_CRT_HERE>"
tls1.key: "<FIRST_BASE64_KEY_HERE>"
tls2.crt: "<SECOND_BASE64_CRT_HERE>"
tls3.key: "<BASE64_BASE64_KEY_HERE>"
+73
View File
@@ -0,0 +1,73 @@
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "100"
aralez.rs/x4xx_limit: "20"
aralez.rs/client_headers: '["X-Proxy-From:Aralezzzzzzzzzzz", "X-Hopar-From:Hopaaaaaaaaaaaar"]'
aralez.rs/server_headers: '["X-Proxy-Version:Aralez", "X-Po-From:Kung Fu Panda"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: api-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service-v2
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Hors-Arev:Naturi", "X-Grand-Master:Oogway"]'
aralez.rs/server_headers: '["X-Mors-Arev:Hastat", "X-Shifu-Master:Shifu"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: joj-service-v2
namespace: upstreams
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: joj-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
+16
View File
@@ -0,0 +1,16 @@
threads: 12
daemon: false
upstream_keepalive_pool_size: 500
pid_file: /tmp/aralez.pid
error_log: /tmp/aralez_err.log
upgrade_sock: /tmp/aralez.sock
config_api_enabled: false
config_address: 127.0.0.1:3000
proxy_address_http: 0.0.0.0:80
proxy_address_tls: 0.0.0.0:443
proxy_configs: /etc/aralez
proxy_tls_grade: high
upstreams_conf: /etc/aralez/upstreams.yml
log_level: info
hc_method: HEAD
hc_interval: 2
+22
View File
@@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-nginx
namespace: upstreams
labels:
app: svc-nginx
spec:
selector:
matchLabels:
app: svc-nginx
replicas: 3
template:
metadata:
labels:
app: svc-nginx
spec:
containers:
- name: svc-nginx
image: nginx:latest
ports:
- containerPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: svc-nginx
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: api-service-v2
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+40
View File
@@ -0,0 +1,40 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: aralez-sa
namespace: aralez
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: aralez-role
rules:
# Watch Ingresses and IngressClasses across all namespaces
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
# Update Ingress status (for setting load balancer IPs)
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["update", "patch"]
# Watch Services and Endpoints across all namespaces
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Watch EndpointSlices across all namespaces
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: aralez-ingress
subjects:
- kind: ServiceAccount
name: aralez-sa
namespace: aralez # <--- MUST match where Aralez is deployed
roleRef:
kind: ClusterRole
name: aralez-role
apiGroup: rbac.authorization.k8s.io
+16
View File
@@ -0,0 +1,16 @@
provider: "kubernetes"
to_https: false
sticky_sessions: 86000
rate_limit: 100
x4xx_limit: 100
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
kubernetes:
services:
- hostname: "svc-nginx"
upstream: "svc-nginx"
path: "/"
tokenpath: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+13 -6
View File
@@ -4,24 +4,31 @@ runuser: aralez # Username for running aralez after dropping root privileges, re
rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/aralez.pid # Path to PID file pid_file: /tmp/aralez.pid # Path to PID file
#error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/aralez.sock # Path to socket file upgrade_sock: /tmp/aralez.sock # Path to socket file
cache_enabled: true # Optional. Disabled by default
cache_storage: memory # memory | redis://host:port . Optional Defaults to memory works with Redis, Valkey, Dragonfly (requires redis:// prefix)
cache_size_mb: 50 # Optional. Set internal cache with size in megabytes of value. Defaults to 50Mb if cache_enabled: true
cache_ttl: 60 # Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_enabled: true
config_api_enabled: true # Boolean to enable/disable remote config push capability. config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 127.0.0.1:3000 # HTTP API address for pushing upstreams.yaml from remote location config_address: 127.0.0.1:3000 # HTTP API address for pushing upstreams.yaml from remote location
proxy_address_http: 0.0.0.0:80 # Proxy HTTP bind address proxy_address_http: 0.0.0.0:80 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:443 # Optional, Proxy TLS bind address proxy_address_tls: 0.0.0.0:443 # Optional, Proxy TLS bind address
proxy_configs: /opt/aralez/asyncweb/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key. proxy_configs: /opt/aralez/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/aralez/public # Optional, local folder to serve file_server_folder: /opt/aralez/public # Optional, local folder to serve
file_server_address: 0.0.0.0:3002 # Optional, Local address for file server. Can set as upstream for public access. file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma.
access_log: error # all, error, (Off if commented) log_pattern: "{d(%Y-%m-%d %H:%M:%S)} {h({l})} {t} - {m} {n}" # Optional log pattern, defaults to "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}"
access_log: error # all, error, (Off by default)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds hc_interval: 2 #Interval for health checks in seconds
tcp_keepalive_idle: 60 # Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client tcp_keepalive_idle: 60 # Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client
tcp_keepalive_interval: 10 # Seconds between individual keepalive probes if the client does not respond tcp_keepalive_interval: 10 # Seconds between individual keepalive probes if the client does not respond
tcp_keepalive_count: 5 # Number of unanswered probes before the kernel declares the connection dead and closes it tcp_keepalive_count: 5 # Number of unanswered probes before the kernel declares the connection dead and closes it
cache_size_mb: 50 # Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out #cluster_member: master | worker Optional. Enables cluster mode, Sets node type to master or worker. Disabled by default
cache_ttl: 120 # Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_size_mb is set #worker_nodes: http://127.0.0.1:8000, http://127.0.0.1:4000 Required is if cluster_member: master. Comma separated list of worker nodes http(s)://host:port
#log_structured: elasticsearch Optional. Enables structured logging and sets backend type. Only elasticsearch et the moment.
#acme_dns_provider: cloudflare Optional. Enable ACME DNS-01 challenge, sets DNS provider. ONly coudflare, route53, digitalocean at the moment.
+1 -1
View File
@@ -130,7 +130,7 @@ upstreams:
"/": "/":
healthcheck: false healthcheck: false
servers: servers:
- "127.0.0.1:3000" - "127.0.0.1:8000"
"/.well-known/acme-challenge": "/.well-known/acme-challenge":
healthcheck: false healthcheck: false
servers: servers:
+16
View File
@@ -0,0 +1,16 @@
use crate::auth::types::AuthValidator;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct ApiKeyAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::auth::types::AuthValidator;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct BasicAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
}
false
}
}
+6 -120
View File
@@ -1,29 +1,14 @@
use crate::utils::jwt::{check_jwt, JWT_TOKEN}; use crate::auth::types::AuthValidator;
use crate::utils::structs::InnerAuth; use crate::utils::tools::split_host_port;
use axum::http::StatusCode; use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora::http::RequestHeader;
use pingora_core::connectors::http::Connector; use pingora_core::connectors::http::Connector;
use pingora_core::upstreams::peer::HttpPeer; use pingora_core::prelude::HttpPeer;
use pingora_http::ResponseHeader; use pingora_http::{RequestHeader, ResponseHeader};
use pingora_proxy::Session; use pingora_proxy::Session;
use std::collections::HashMap;
use std::sync::LazyLock; use std::sync::LazyLock;
use subtle::ConstantTimeEq;
use urlencoding::decode;
#[async_trait::async_trait]
trait AuthValidator {
async fn validate(&self, session: &mut Session) -> bool;
}
struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth();
struct ForwardAuth<'a>(&'a str);
pub static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub struct ForwardAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait] #[async_trait::async_trait]
impl AuthValidator for ForwardAuth<'_> { impl AuthValidator for ForwardAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool { async fn validate(&self, session: &mut Session) -> bool {
@@ -144,102 +129,3 @@ impl AuthValidator for ForwardAuth<'_> {
} }
} }
} }
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
}
false
}
}
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&*auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&*auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&*auth.auth_cred).validate(session).await,
_ => {
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
}
#[allow(clippy::needless_return)]
fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)),
Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr);
return None;
}
},
None => {
if tls {
return Some((addr, 443u16, tls, addr));
} else {
return Some((addr, 80u16, tls, addr));
}
}
};
}
+44 -3
View File
@@ -1,13 +1,18 @@
use crate::auth::types::AuthValidator;
use ahash::AHasher; use ahash::AHasher;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use base64::Engine;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use moka::sync::Cache; use moka::sync::Cache;
use moka::Expiry; use moka::Expiry;
use pingora_proxy::Session;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env; use std::env;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant, SystemTime}; use std::time::{Duration, Instant, SystemTime};
use urlencoding::decode;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Claims { pub struct Claims {
@@ -41,6 +46,42 @@ impl Expiry<u64, u64> for JwtExpiry {
} }
} }
} }
pub struct JwtAuth();
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
}
pub fn check_jwt(token: &str, secret: &str) -> bool { pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret); let key = hash_token(token, secret);
@@ -57,7 +98,7 @@ pub fn check_jwt(token: &str, secret: &str) -> bool {
Err(_) => return false, Err(_) => return false,
} }
match decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) { match jsonwebtoken::decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => { Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now { if data.claims.exp > now {
+7
View File
@@ -0,0 +1,7 @@
pub mod apikey;
pub mod basic;
pub mod forward;
pub mod jwt;
pub mod types;
pub use types::*;
+23
View File
@@ -0,0 +1,23 @@
use crate::auth::apikey::ApiKeyAuth;
use crate::auth::basic::BasicAuth;
use crate::auth::forward::ForwardAuth;
use crate::auth::jwt::JwtAuth;
use crate::utils::types::InnerAuth;
use pingora_proxy::Session;
#[async_trait::async_trait]
pub trait AuthValidator {
async fn validate(&self, session: &mut Session) -> bool;
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&auth.auth_cred).validate(session).await,
_ => {
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod redis;
pub mod types;
+222
View File
@@ -0,0 +1,222 @@
use async_trait::async_trait;
use bytes::Bytes;
use log::warn;
use pingora::Result;
use pingora_cache::storage::{HandleHit, HandleMiss, HitHandler, MissFinishType, MissHandler, Storage};
use pingora_cache::trace::SpanHandle;
use pingora_cache::{CacheKey, CacheMeta, PurgeOutcome, PurgeTarget, PurgeType};
use redis::aio::ConnectionManager;
use redis::{AsyncCommands, Client, RedisResult};
use std::any::Any;
use std::borrow::Cow;
use std::sync::Arc;
#[derive(Clone)]
pub struct RedisCache {
pub client: Arc<ConnectionManager>,
pub ttl: u64,
}
pub struct RedisHitHandler {
body: Option<Bytes>,
}
pub struct RedisMissHandler {
client: Arc<ConnectionManager>,
body_key: String,
body_buffer: Vec<u8>,
ttl: u64,
}
impl RedisCache {
pub async fn new(url: &str, ttl: u64) -> RedisResult<Self> {
let client = Client::open(url)?;
let manager = ConnectionManager::new(client).await?;
Ok(Self { client: Arc::new(manager), ttl })
}
fn make_key(key: &CacheKey) -> Cow<'_, str> {
match key.primary_key_str() {
Some(s) => Cow::Borrowed(s),
None => Cow::Owned(key.primary_key().iter().map(|b| format!("{:02x}", b)).collect()),
}
}
}
#[async_trait]
impl HandleHit for RedisHitHandler {
async fn read_body(&mut self) -> Result<Option<Bytes>> {
Ok(self.body.take())
}
async fn finish(self: Box<Self>, _storage: &'static (dyn Storage + Sync), _key: &CacheKey, _trace: &SpanHandle) -> Result<()> {
Ok(())
}
fn can_seek(&self) -> bool {
false
}
fn seek(&mut self, _start: usize, _end: Option<usize>) -> Result<()> {
Ok(())
}
fn should_count_access(&self) -> bool {
true
}
fn get_eviction_weight(&self) -> usize {
self.body.as_ref().map(|b| b.len()).unwrap_or(0)
}
fn entry_id(&self) -> Option<pingora_cache::eviction::CacheEntryId> {
None
}
fn as_any(&self) -> &(dyn Any + Send + Sync) {
self
}
fn as_any_mut(&mut self) -> &mut (dyn Any + Send + Sync) {
self
}
}
#[async_trait]
impl HandleMiss for RedisMissHandler {
async fn write_body(&mut self, data: Bytes, _eof: bool) -> Result<()> {
self.body_buffer.extend_from_slice(&data);
Ok(())
}
async fn finish(self: Box<Self>) -> Result<MissFinishType> {
let len = self.body_buffer.len();
let mut conn = (*self.client).clone();
if let Err(e) = conn.set_ex::<_, _, ()>(&self.body_key, &self.body_buffer, self.ttl).await {
warn!("Failed to write cache entry to Redis: {e}");
}
Ok(MissFinishType::Appended(len, None))
}
fn entry_id(&self) -> Option<pingora_cache::eviction::CacheEntryId> {
None
}
}
#[async_trait]
impl Storage for RedisCache {
async fn lookup(&'static self, key: &CacheKey, _trace: &SpanHandle) -> Result<Option<(CacheMeta, HitHandler)>> {
let base = Self::make_key(key);
// Pre-allocate exact capacities to prevent reallocations
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut body_key = String::with_capacity(base.len() + 5);
body_key.push_str(&base);
body_key.push_str(":body");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let mut conn = (*self.client).clone();
let internal: Option<Vec<u8>> = conn.get(&internal_key).await.ok().flatten();
let header: Option<Vec<u8>> = conn.get(&header_key).await.ok().flatten();
let body: Option<Vec<u8>> = conn.get(&body_key).await.ok().flatten();
match (internal, header, body) {
(Some(internal_bytes), Some(header_bytes), Some(body_bytes)) => match CacheMeta::deserialize(&internal_bytes, &header_bytes) {
Ok(meta) => {
let handler: HitHandler = Box::new(RedisHitHandler {
body: Some(Bytes::from(body_bytes)),
});
Ok(Some((meta, handler)))
}
Err(_) => {
let _: RedisResult<()> = conn.del(&[&internal_key, &header_key, &body_key]).await;
Ok(None)
}
},
_ => Ok(None),
}
}
async fn get_miss_handler(&'static self, key: &CacheKey, meta: &CacheMeta, _trace: &SpanHandle) -> Result<MissHandler> {
let base = Self::make_key(key);
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut body_key = String::with_capacity(base.len() + 5);
body_key.push_str(&base);
body_key.push_str(":body");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let ttl = self.ttl;
let (internal_bytes, header_bytes) = meta.serialize()?;
let mut conn = (*self.client).clone();
let _: RedisResult<()> = conn.set_ex(&internal_key, &internal_bytes, ttl).await;
let _: RedisResult<()> = conn.set_ex(&header_key, &header_bytes, ttl).await;
Ok(Box::new(RedisMissHandler {
client: self.client.clone(),
body_key,
body_buffer: Vec::with_capacity(4096),
ttl,
}))
}
async fn purge(&'static self, _target: PurgeTarget<'_>, _purge_type: PurgeType, _trace: &SpanHandle) -> Result<PurgeOutcome> {
Ok(PurgeOutcome::NotFound)
}
async fn update_meta(&'static self, key: &CacheKey, meta: &CacheMeta, _trace: &SpanHandle) -> Result<bool> {
let base = Self::make_key(key);
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let ttl = self.ttl;
match meta.serialize() {
Ok((internal_bytes, header_bytes)) => {
let mut conn = (*self.client).clone();
let _: RedisResult<()> = conn.set_ex(&internal_key, &internal_bytes, ttl).await;
let _: RedisResult<()> = conn.set_ex(&header_key, &header_bytes, ttl).await;
Ok(true)
}
Err(_) => Ok(false),
}
}
fn support_streaming_partial_write(&self) -> bool {
false
}
fn as_any(&self) -> &(dyn Any + Send + Sync) {
self
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::cache::redis::RedisCache;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::types::AppConfig;
use log::{info, warn};
use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::{MemCache, Storage};
use std::sync::{Arc, OnceLock};
pub static GLOBAL_STORAGE: OnceLock<Box<dyn Storage + Send + Sync>> = OnceLock::new();
pub async fn init_cache(config: Arc<AppConfig>) {
let cache_storage = config.cache_storage.clone().unwrap_or_else(|| {
info!("No cache_storage configured, defaulting to memory");
"memory".to_string()
});
let cache_type = cache_storage.splitn(2, ':').next().unwrap_or("memory");
let ttl = config.cache_ttl.unwrap_or(60);
match cache_type {
"memory" => {
CACHE_TTL.set(ttl).ok();
if let Some(size) = config.cache_size_mb {
let cache_size = size * 1024 * 1024;
drop(EVICTION.set(Manager::new(cache_size)));
}
GLOBAL_STORAGE.set(Box::new(MemCache::new())).ok();
info!("Cache initialized successfully. Storage: {}", cache_storage);
}
"redis" => match RedisCache::new(&cache_storage, ttl).await {
Ok(cache) => {
CACHE_TTL.set(ttl).ok();
GLOBAL_STORAGE.set(Box::new(cache)).ok();
info!("Cache initialized successfully. Storage: {}", cache_storage);
}
Err(e) => log::error!("Cache storage initialization failed: {}", e),
},
_ => {
warn!("Unsupported storage, disabling caching: {}", cache_storage);
}
}
}
+26 -14
View File
@@ -1,38 +1,44 @@
use crate::cache::types::init_cache;
use crate::core::proxyhttp::LB;
use crate::ingress::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::logging::core::{init_access_log, init_access_logging, log_builder};
use crate::tls::acme::order::refresh_order; use crate::tls::acme::order::refresh_order;
use crate::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider}; use crate::utils::metrics::calc_cache_metrics;
use crate::utils::parceyaml::load_configuration; use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::utils::types::Configuration;
use crate::utils::*; use crate::utils::*;
use crate::web::logging::init_logging;
use crate::web::proxyhttp::LB;
use async_trait::async_trait; use async_trait::async_trait;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info}; use log::{error, info};
use pingora_core::server::ShutdownWatch; use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService; use pingora_core::services::background::BackgroundService;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[async_trait] #[async_trait]
impl BackgroundService for LB { impl BackgroundService for LB {
async fn start(&self, mut shutdown: ShutdownWatch) { async fn start(&self, mut shutdown: ShutdownWatch) {
log_builder(&self.config, &self.config.log_file).await;
let al = &self.config.access_log.clone().unwrap_or("none".to_string());
let _ = init_access_log(al.as_str()).await;
info!("Starting background service"); // tx: Sender<Configuration> info!("Starting background service"); // tx: Sender<Configuration>
let (tx, mut rx) = mpsc::channel::<Configuration>(1); let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone(); let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath") let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await .await
.0 .0
.expect("Failed to load configuration"); .die("Failed to load configuration");
match config.typecfg.as_str() { match config.typecfg.as_str() {
"file" => { "file" => {
info!("Running File discovery, requested type is: {}", config.typecfg); info!("Running File discovery, requested type is: {}", config.typecfg);
tx.send(config).await.unwrap(); tx.send(config).await.die("Running File discovery");
let file_load = FromFileProvider { let file_load = FromFileProvider {
path: self.config.upstreams_conf.clone(), path: self.config.upstreams_conf.clone(),
}; };
// let _ = tokio::spawn(async move { file_load.start(tx).await });
drop(tokio::spawn(async move { file_load.start(tx).await })); drop(tokio::spawn(async move { file_load.start(tx).await }));
} }
"kubernetes" => { "kubernetes" => {
@@ -66,24 +72,27 @@ impl BackgroundService for LB {
file_server_folder: self.config.file_server_folder.clone(), file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.clone(), current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(), full_upstreams: self.ump_full.clone(),
acme_dns_provider: self.config.acme_dns_provider.clone(),
}; };
drop(tokio::spawn(async move { api_load.start(tx_api).await })); tokio::spawn(async move { api_load.start(tx_api).await });
let uu = self.ump_upst.clone(); let uu = self.ump_upst.clone();
let ff = self.ump_full.clone(); let ff = self.ump_full.clone();
let im = self.ump_byid.clone(); let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval); let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
drop(tokio::spawn(async move { tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().die("Unable to spawn HC worker"))).await });
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await tokio::spawn(async move { calc_cache_metrics().await });
})); let acme_provider = self.config.acme_dns_provider.clone();
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await })); tokio::spawn(async move { refresh_order(certdir, confdir, acme_provider).await });
init_logging(self.config.access_log.clone());
init_cache(self.config.clone()).await;
init_access_logging(self.config.access_log.clone()).await;
loop { loop {
tokio::select! { tokio::select! {
_ = shutdown.changed() => { _ = shutdown.changed() => {
break; break;
} }
val = rx.recv() => { val = rx.recv() => {
let start = SystemTime::now().duration_since(UNIX_EPOCH).die(format!("ON {} {}", file!(), line!()).as_str());
if let Some(ss) = val { if let Some(ss) = val {
clone_dashmap_into(&ss.upstreams, &self.ump_full); clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst); clone_dashmap_into(&ss.upstreams, &self.ump_upst);
@@ -130,6 +139,9 @@ impl BackgroundService for LB {
} }
} }
} }
let end = SystemTime::now().duration_since(UNIX_EPOCH).die(format!("ON {} {}", file!(), line!()).as_str());
let startup = end - start;
info!("Loading upstreams took: {}ms", startup.as_millis());
} }
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::utils::structs::InnerMap; use crate::core::proxyhttp::LB;
use crate::web::proxyhttp::LB; use crate::utils::types::InnerMap;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
-2
View File
@@ -1,7 +1,5 @@
pub mod acme;
pub mod bgservice; pub mod bgservice;
pub mod gethosts; pub mod gethosts;
pub mod logging;
pub mod proxyhttp; pub mod proxyhttp;
pub mod start; pub mod start;
pub mod webserver; pub mod webserver;
+69 -68
View File
@@ -1,16 +1,17 @@
use crate::utils::auth::authenticate; use crate::auth::authenticate;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE}; use crate::cache::types::GLOBAL_STORAGE;
use crate::core::gethosts::{GetHost, GetHostsReturHeaders};
use crate::logging::core::access_log;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*; use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::types::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use crate::web::logging::access_log;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use async_trait::async_trait; use async_trait::async_trait;
use axum::body::Bytes; use axum::body::Bytes;
use log::error; use log::error;
use pingora::ErrorSource::Upstream;
use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode}; use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*; use pingora::prelude::*;
use pingora::ErrorSource::Upstream;
use pingora_cache::cache_control::CacheControl; use pingora_cache::cache_control::CacheControl;
use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable}; use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable};
use pingora_core::listeners::ALPN; use pingora_core::listeners::ALPN;
@@ -34,6 +35,7 @@ pub struct LB {
pub config: Arc<AppConfig>, pub config: Arc<AppConfig>,
pub extraparams: Arc<ArcSwap<Extraparams>>, pub extraparams: Arc<ArcSwap<Extraparams>>,
pub cache_enabled: bool, pub cache_enabled: bool,
pub master_server: bool,
} }
pub struct Context { pub struct Context {
@@ -43,9 +45,12 @@ pub struct Context {
upstream_peer: Option<Arc<InnerMap>>, upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>, extraparams: arc_swap::Guard<Arc<Extraparams>>,
client_headers: Option<Vec<(String, Arc<str>)>>, client_headers: Option<Vec<(String, Arc<str>)>>,
x4xx_limit: Option<u32>, x4xx_limit: u32,
} }
thread_local! {
static KEY_BUF: RefCell<String> = RefCell::new(String::with_capacity(256));
}
#[async_trait] #[async_trait]
impl ProxyHttp for LB { impl ProxyHttp for LB {
type CTX = Context; type CTX = Context;
@@ -57,17 +62,28 @@ impl ProxyHttp for LB {
upstream_peer: None, upstream_peer: None,
extraparams: self.extraparams.load(), extraparams: self.extraparams.load(),
client_headers: None, client_headers: None,
x4xx_limit: None, x4xx_limit: 0,
} }
} }
fn cache_key_callback(&self, session: &Session, _ctx: &mut Self::CTX) -> Result<CacheKey> { fn cache_key_callback(&self, session: &Session, ctx: &mut Self::CTX) -> Result<CacheKey> {
Ok(CacheKey::new(_ctx.hostname.clone().unwrap_or_default().as_ref(), session.req_header().uri.to_string(), "")) let host = ctx.hostname.as_deref().unwrap_or("default");
let path = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str());
let primary_key = KEY_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
b.push_str(host);
b.push_str(path);
b.clone()
});
Ok(CacheKey::new(primary_key, host /* host is just for debugging */))
} }
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> { fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
if self.cache_enabled && session.req_header().method == Method::GET { let has_auth = _ctx.upstream_peer.as_ref().map(|m| m.authorization.is_some()).unwrap_or(false) || _ctx.extraparams.authentication.is_some();
if let Some(eviction) = EVICTION.get() { if self.cache_enabled && session.req_header().method == Method::GET && !has_auth {
session.cache.enable(&*MEM_CACHE, Some(eviction), None, Some(&*CACHE_LOCK), None); if let Some(storage) = GLOBAL_STORAGE.get() {
let eviction_mgr = EVICTION.get().map(|e| e as &(dyn pingora_cache::eviction::EvictionManager + Sync));
session.cache.enable(storage.as_ref(), eviction_mgr, None, Some(&*CACHE_LOCK), None);
} }
} }
Ok(()) Ok(())
@@ -89,11 +105,15 @@ impl ProxyHttp for LB {
} }
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> { async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
if self.master_server {
let _ = session.respond_error(400).await;
return Ok(true);
};
ACTIVE_SESSIONS.inc(); ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst); let hostname = return_header_host_from_upstream(session, &self.ump_upst);
_ctx.hostname = hostname; _ctx.hostname = hostname;
let mut backend_id = None; let mut backend_id = None;
if let Some(_) = _ctx.extraparams.sticky_sessions { if _ctx.extraparams.sticky_sessions > 0 {
if let Some(cookies) = session.req_header().headers.get("cookie") { if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() { if let Ok(cookie_str) = cookies.to_str() {
if let Some(pos) = cookie_str.find("backend_id=") { if let Some(pos) = cookie_str.find("backend_id=") {
@@ -112,17 +132,30 @@ impl ProxyHttp for LB {
None => return Ok(false), None => return Ok(false),
Some(ref innermap) => { Some(ref innermap) => {
if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) { if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) {
if !authenticate(&auth, session).await { if !authenticate(auth, session).await {
let _ = session.respond_error(401).await; let _ = session.respond_error(401).await;
return Ok(true); return Ok(true);
} }
} }
if let Some(rate) = innermap.x4xx_limit.or(_ctx.extraparams.x4xx_limit) { let rate = if innermap.rate_limit > 0 { innermap.rate_limit } else { _ctx.extraparams.rate_limit };
_ctx.x4xx_limit = innermap.x4xx_limit; let xrate = if innermap.x4xx_limit > 0 { innermap.x4xx_limit } else { _ctx.extraparams.x4xx_limit };
if xrate > 0 || rate > 0 {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip()); let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
if let Some(rk) = rate_key { if xrate > 0 {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0); _ctx.x4xx_limit = xrate;
if count > rate { if let Some(rk) = rate_key {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0);
if count > xrate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
}
if rate > 0 {
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate {
let header = ResponseHeader::build(429, None)?; let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None); session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?; session.write_response_header(Box::new(header), true).await?;
@@ -130,16 +163,6 @@ impl ProxyHttp for LB {
} }
} }
} }
if let Some(rate) = innermap.rate_limit.or(_ctx.extraparams.rate_limit) {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
if let Some(redirect_to) = &innermap.redirect_to { if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path(); let uri = session.req_header().uri.path();
@@ -154,7 +177,7 @@ impl ProxyHttp for LB {
return Ok(true); return Ok(true);
} }
if _ctx.extraparams.to_https.unwrap_or(false) || innermap.to_https { if _ctx.extraparams.to_https || innermap.to_https {
if let Some(stream) = session.stream() { if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() { if stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() { if let Some(host) = _ctx.hostname.as_ref() {
@@ -197,21 +220,7 @@ impl ProxyHttp for LB {
peer.options.verify_cert = false; peer.options.verify_cert = false;
peer.options.verify_hostname = false; peer.options.verify_hostname = false;
} }
/* if ctx.extraparams.sticky_sessions > 0 {
Experimental optionsv
The following TCP optimizations were tested but caused performance degrade under heavy load:
peer.options.tcp_keepalive = Some(TcpKeepalive {
idle: Duration::from_secs(60),
interval: Duration::from_secs(10),
count: 5,
user_timeout: Duration::from_secs(30),
});
peer.options.idle_timeout = Some(Duration::from_secs(300));
peer.options.tcp_recv_buf = Some(128 * 1024);
End of experimental options
*/
if let Some(_) = ctx.extraparams.sticky_sessions {
let mut s = String::with_capacity(64); let mut s = String::with_capacity(64);
write!( write!(
&mut s, &mut s,
@@ -221,9 +230,9 @@ impl ProxyHttp for LB {
innermap.port, innermap.port,
innermap.is_http2, innermap.is_http2,
innermap.to_https, innermap.to_https,
innermap.x4xx_limit.unwrap_or_default(), innermap.x4xx_limit,
innermap.rate_limit.unwrap_or_default(), innermap.rate_limit,
innermap.healthcheck.unwrap_or_default(), innermap.healthcheck,
innermap.authorization innermap.authorization
) )
.unwrap_or(()); .unwrap_or(());
@@ -287,7 +296,7 @@ impl ProxyHttp for LB {
Ok(()) Ok(())
} }
async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> { async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(val) = ctx.extraparams.sticky_sessions { if ctx.extraparams.sticky_sessions > 0 {
if let Some(bid) = &ctx.backend_id { if let Some(bid) = &ctx.backend_id {
let tt = if let Some(existing) = REVERSE_STORE.get(bid) { let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
existing.value().clone() existing.value().clone()
@@ -305,20 +314,21 @@ impl ProxyHttp for LB {
buf.push_str("backend_id="); buf.push_str("backend_id=");
buf.push_str(&tt); buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age="); buf.push_str("; Path=/; Max-Age=");
buf.push_str(&val.to_string()); buf.push_str(&ctx.extraparams.sticky_sessions.to_string());
buf.push_str("; HttpOnly; SameSite=Lax"); buf.push_str("; HttpOnly; SameSite=Lax");
let _ = _upstream_response.append_header("set-cookie", buf.as_str()); let _ = _upstream_response.append_header("set-cookie", buf.as_str());
} }
} }
Ok(())
}
async fn upstream_response_filter(&self, _session: &mut Session, upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(client_headers) = &ctx.client_headers { if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() { for (k, v) in client_headers.iter() {
_upstream_response.append_header(k.clone(), v.as_ref())?; upstream_response.append_header(k.clone(), v.as_ref())?;
} }
} }
Ok(()) Ok(())
} }
async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) { async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) {
let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16()); let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
let m = &MetricTypes { let m = &MetricTypes {
@@ -330,22 +340,13 @@ impl ProxyHttp for LB {
}; };
calc_metrics(m); calc_metrics(m);
ACTIVE_SESSIONS.dec(); ACTIVE_SESSIONS.dec();
if let Some(_) = ctx.x4xx_limit.or(ctx.extraparams.x4xx_limit) { if ctx.x4xx_limit > 0 && (400..=499).contains(&response_code) {
if (400..=499).contains(&response_code) { if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) { let current = REQUESTS_4XX.get(&ip).unwrap_or(0);
let current = REQUESTS_4XX.get(&ip).unwrap_or(0); REQUESTS_4XX.insert(ip, current + 1);
REQUESTS_4XX.insert(ip, current + 1);
}
} }
} }
// match session.cache.phase() { access_log(response_code, &self.request_summary(session, ctx), session).await;
// CachePhase::Hit => info!("Cache Status: HIT (Served from Memory)"),
// CachePhase::Miss => info!("Cache Status: MISS (Fetched from Upstream)"),
// CachePhase::Expired => info!("Cache Status: EXPIRED (Revalidating)"),
// _ => {}
// }
access_log(response_code, &self.request_summary(session, ctx), session);
} }
} }
+36 -35
View File
@@ -1,47 +1,52 @@
// use rustls::crypto::ring::default_provider; use crate::core::proxyhttp::LB;
use crate::tls::grades; use crate::tls::grades;
use crate::tls::load; use crate::tls::load;
use crate::tls::load::CertificateConfig; use crate::tls::load::CertificateConfig;
use crate::utils::folder_watch;
use crate::utils::lazylock::{CACHE_TTL, EVICTION}; use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::structs::Extraparams;
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::web::logging::init_access_log; use crate::utils::types::Extraparams;
use crate::web::proxyhttp::LB;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use dashmap::DashMap; use dashmap::DashMap;
use log::info; use log::info;
use pingora::tls::ssl::{SslAlert, SslRef}; use pingora::tls::ssl::{SslAlert, SslRef};
use pingora_cache::eviction::simple_lru::Manager; use pingora_cache::eviction::simple_lru::Manager;
use pingora_core::listeners::tls::TlsSettings;
use pingora_core::listeners::TcpSocketOptions; use pingora_core::listeners::TcpSocketOptions;
use pingora_core::prelude::{background_service, Opt}; use pingora_core::listeners::tls::TlsSettings;
use pingora_core::prelude::{Opt, background_service};
use pingora_core::protocols::TcpKeepalive; use pingora_core::protocols::TcpKeepalive;
use pingora_core::server::Server; use pingora_core::server::Server;
use privdrop::reexports::libc::SIGQUIT; use privdrop::reexports::libc::SIGQUIT;
use rustls::crypto::ring::default_provider;
use sd_notify::NotifyState; use sd_notify::NotifyState;
use signal_hook::{ use signal_hook::{
consts::{SIGINT, SIGTERM}, consts::{SIGINT, SIGTERM},
iterator::Signals, iterator::Signals,
}; };
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration; use std::time::Duration;
use std::{fs, thread}; use std::{fs, thread};
pub fn run() { pub fn run() {
// default_provider().install_default().expect("Failed to install rustls crypto provider"); default_provider().install_default().die("Failed to install RusTLS crypto provider:");
let parameters = Opt::parse_args(); let parameters = Opt::parse_args();
let file = parameters.conf.clone().unwrap(); let file = parameters.conf.clone().die("Failed to clone the config:");
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str()); let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
let cache_enabled = maincfg.cache_size_mb.is_some(); let master_server = maincfg.cluster_member.as_deref() == Some("master");
CACHE_TTL.set(maincfg.cache_ttl.unwrap_or(60)).unwrap();
if let Some(size) = maincfg.cache_size_mb {
let cache_size = size * 1024 * 1024;
drop(EVICTION.set(Manager::new(cache_size)));
}
let mut server = Server::new(parameters).unwrap(); let cache_enabled = maincfg.cache_enabled.unwrap_or(false);
// ========================================================================== //
CACHE_TTL.set(maincfg.cache_ttl.unwrap_or(60)).die("Failed to set cache TTL");
if cache_enabled {
let cache_size = maincfg.cache_size_mb.unwrap_or(50);
let _ = EVICTION.set(Manager::new(cache_size * 1024 * 1024));
}
// ========================================================================== //
let mut server = Server::new(parameters).die("Failed to start the server:");
server.bootstrap(); server.bootstrap();
let uf_config = Arc::new(DashMap::new()); let uf_config = Arc::new(DashMap::new());
@@ -51,11 +56,12 @@ pub fn run() {
let sh_config = Arc::new(DashMap::new()); let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams { let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
to_https: None, to_https: false,
sticky_sessions: None, ssl_verify: false,
sticky_sessions: 0,
authentication: None, authentication: None,
rate_limit: None, rate_limit: 0,
x4xx_limit: None, x4xx_limit: 0,
})); }));
let cfg = Arc::new(maincfg); let cfg = Arc::new(maincfg);
@@ -68,10 +74,9 @@ pub fn run() {
client_headers: ch_config, client_headers: ch_config,
server_headers: sh_config, server_headers: sh_config,
extraparams: ec_config, extraparams: ec_config,
cache_enabled: cache_enabled, cache_enabled,
master_server,
}; };
let al = cfg.access_log.clone().unwrap_or("none".to_string());
init_access_log(al.as_str());
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string()); let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
info!("TLS grade set to: [ {} ]", grade); info!("TLS grade set to: [ {} ]", grade);
@@ -84,9 +89,6 @@ pub fn run() {
check_priv(bind_address_http.as_str()); check_priv(bind_address_http.as_str());
// let mut tcp_options: Option<TcpSocketOptions> = Some(TcpSocketOptions::default());
// let mut tcp_options = TcpSocketOptions::default();
let mut tcp_options: Option<TcpSocketOptions> = None; let mut tcp_options: Option<TcpSocketOptions> = None;
if let Some(idle) = cfg.tcp_keepalive_idle { if let Some(idle) = cfg.tcp_keepalive_idle {
let mut to = TcpSocketOptions::default(); let mut to = TcpSocketOptions::default();
@@ -108,25 +110,25 @@ pub fn run() {
if let Some(bind_address_tls) = bind_address_tls { if let Some(bind_address_tls) = bind_address_tls {
check_priv(bind_address_tls.as_str()); check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel(); let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_configs.clone().unwrap() + "/certificates"; let certs_path = cfg.proxy_configs.clone().die("Failed certificates path") + "/certificates";
if fs::metadata(certs_path.clone()).is_err() { if fs::metadata(certs_path.clone()).is_err() {
fs::create_dir_all(certs_path.clone()).unwrap(); fs::create_dir_all(certs_path.clone()).die("Failed to create directories");
} }
thread::spawn(move || { thread::spawn(move || {
watch_folder(certs_path, tx).unwrap(); folder_watch(certs_path, tx).die("Failed to spawn certificates channel");
}); });
let certificate_configs = rx.recv().unwrap(); let certificate_configs = rx.recv().die("Failed to spawn certificates channel");
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info")); let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set)); let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone(); let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone(); let certs_for_watcher = certificates.clone();
let new_certs = load::Certificates::new(&certificate_configs, grade.as_str()); let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.unwrap())); certs_for_watcher.store(Arc::new(new_certs.die("Failed to build the watcher")));
let mut tls_settings = let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).expect("unable to load or parse cert/key"); TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).die("unable to load or parse cert/key");
grades::set_tsl_grade(&mut tls_settings, grade.as_str()); grades::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert)); tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
@@ -164,8 +166,8 @@ pub fn run() {
if let Err(e) = write_pid_file(pf.as_str()) { if let Err(e) = write_pid_file(pf.as_str()) {
panic!("Failed to write PID file: {} : {}", pf, e); panic!("Failed to write PID file: {} : {}", pf, e);
} }
let mut signals = Signals::new(&[SIGINT, SIGTERM, SIGQUIT]).unwrap(); let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT]).die("Failed to create signals");
for sig in signals.forever() { if let Some(sig) = signals.forever().next() {
match sig { match sig {
SIGINT => info!("SIGINT received! Exiting..."), SIGINT => info!("SIGINT received! Exiting..."),
SIGTERM => info!("SIGTERM received! Exiting..."), SIGTERM => info!("SIGTERM received! Exiting..."),
@@ -175,6 +177,5 @@ pub fn run() {
} }
_ => unreachable!(), _ => unreachable!(),
} }
break;
} }
} }
+91 -53
View File
@@ -1,18 +1,21 @@
use crate::utils::discovery::APIUpstreamProvider; use crate::auth::jwt::Claims;
use crate::utils::jwt::Claims; use crate::ingress::APIUpstreamProvider;
use crate::utils::metrics::{get_memory_usage, get_open_files, MEMORY_USAGE, OPEN_FILES}; use crate::tls::acme::http01::{acme_create, acme_order, http01_challenge};
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap}; use crate::utils::lazylock::NODE_STATUS;
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json}; use crate::utils::metrics::{MEMORY_USAGE, OPEN_FILES, get_memory_usage, get_open_files};
use crate::web::acme::{acme_create, acme_order, http01_challenge}; use crate::utils::tools::{Die, upstreams_liveness_json, upstreams_to_json};
use crate::utils::types::{Config, Configuration, UpstreamsDashMap};
use axum::body::Body; use axum::body::Body;
use axum::extract::ConnectInfo;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::HeaderMap;
use axum::http::{Response, StatusCode}; use axum::http::{Response, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{any, get, post}; use axum::routing::{any, get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use jsonwebtoken::{encode, EncodingKey, Header}; use jsonwebtoken::{EncodingKey, Header, encode};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use prometheus::{gather, Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder, gather};
use serde::Serialize; use serde::Serialize;
use signal_hook::{consts::SIGQUIT, iterator::Signals}; use signal_hook::{consts::SIGQUIT, iterator::Signals};
use std::collections::HashMap; use std::collections::HashMap;
@@ -30,10 +33,11 @@ struct OutToken {
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct AppState { pub struct AppState {
master_key: Option<String>, master_key: Option<String>,
pub(crate) cert_creds: String, pub acme_dns_provider: Option<String>,
pub(crate) certs_dir: String, pub cert_creds: String,
pub certs_dir: String,
upstreams_file: String, upstreams_file: String,
config_sender: mpsc::Sender<Configuration>, config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool, config_api_enabled: bool,
@@ -53,6 +57,7 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sende
config_api_enabled: config.config_api_enabled, config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr, current_upstreams: upstreams_curr,
full_upstreams: upstreams_full, full_upstreams: upstreams_full,
acme_dns_provider: config.acme_dns_provider.clone(),
}; };
let app = Router::new() let app = Router::new()
// .route("/{*wildcard}", get(senderror)) // .route("/{*wildcard}", get(senderror))
@@ -61,13 +66,14 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sende
.route("/acme_order/{*domain}", any(acme_order)) .route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge)) .route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf)) .route("/conf", post(conf))
.route("/workerconf", post(workerconf))
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.route("/status", get(status)) .route("/status", get(status))
.with_state(app_state); .with_state(app_state);
let mut static_handle: Option<tokio::task::JoinHandle<()>> = None; let mut static_handle: Option<tokio::task::JoinHandle<()>> = None;
if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) { if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_listen = port_is_available("File Server", &address).await; let static_listen = port_is_available("File Server", address).await;
let static_files = ServeDir::new(folder); let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files); let static_serve: Router = Router::new().fallback_service(static_files);
info!("Starting file server on: {}", address); info!("Starting file server on: {}", address);
@@ -78,22 +84,20 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sende
Err(e) => error!("File server exited: {}", e), Err(e) => error!("File server exited: {}", e),
} }
})) }))
// let a = config.file_server_address.to_owned();
// let f = config.file_server_folder.to_owned();
// drop(tokio::spawn(async move { run_file_server(a, f).await }));
} }
let listener = port_is_available("Config API", &config.address).await; let listener = port_is_available("Config API", &config.address).await;
info!("Starting the API server on: {}", config.address); info!("Starting the API server on: {}", config.address);
let api_server = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); let api_server = tokio::spawn(async move {
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.await
.die("Can't start API server")
});
let (tx, mut rx) = mpsc::channel(1); let (tx, mut rx) = mpsc::channel(1);
std::thread::spawn(move || { std::thread::spawn(move || {
let mut signals = Signals::new(&[SIGQUIT]).unwrap(); let mut signals = Signals::new([SIGQUIT]).die("Can't initialize OS SIGNALS");
for sig in signals.forever() { if let Some(sig) = signals.forever().next() {
tx.blocking_send(sig).unwrap(); tx.blocking_send(sig).die("Can;t send signal to main thread");
break;
} }
}); });
rx.recv().await; rx.recv().await;
@@ -104,29 +108,79 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sende
info!("Exiting..."); info!("Exiting...");
} }
async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>, content: String) -> impl IntoResponse { async fn conf(
State(st): State<AppState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(params): Query<HashMap<String, String>>,
headers: HeaderMap,
content: String,
) -> impl IntoResponse {
if !st.config_api_enabled { if !st.config_api_enabled {
return Response::builder().status(StatusCode::FORBIDDEN).body(Body::from("Config API is disabled !\n")).unwrap(); return (StatusCode::FORBIDDEN, "Config API is disabled !\n").into_response();
}
let api_key = headers.get("API-KEY").and_then(|v| v.to_str().ok());
let query_key = params.get("key").map(|k| k.as_str());
let authorized = match st.master_key.as_deref() {
Some(master_key) if !master_key.is_empty() => api_key == Some(master_key) || query_key == Some(master_key),
_ => false,
};
if !authorized {
warn!("Unauthorised access to /conf endpoint from {}", addr.ip());
return (StatusCode::UNAUTHORIZED, "Unauthorized: Invalid API-KEY\n").into_response();
} }
let strcontent = content.as_str(); let strcontent = content.as_str();
let parsed = noyalib::from_str::<Config>(strcontent); let parsed = noyalib::from_str::<Config>(strcontent);
match parsed { match parsed {
Ok(_) => { Ok(_) => {
if let Some(_) = params.get("save") { let save = params.contains_key("save");
drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await })); tokio::spawn(async move {
} else { apply_config(&content, st, save).await;
drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await })); });
} (StatusCode::OK, "Accepted! Applying in background\n").into_response()
Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap()
} }
Err(err) => { Err(err) => {
error!("Failed to parse upstreams file: {}", err); error!("Failed to parse upstreams file: {}", err);
Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from(format!("Failed: {}\n", err))).unwrap() (StatusCode::BAD_REQUEST, format!("Failed: {}\n", err)).into_response()
} }
} }
} }
async fn workerconf(State(st): State<AppState>, ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap, content: String) -> impl IntoResponse {
let api_key = headers.get("API-KEY").and_then(|v| v.to_str().ok());
if api_key != st.master_key.as_deref() {
warn!("Unauthorised access to workerconf endpoint from {}", addr.ip());
return (StatusCode::UNAUTHORIZED, "Unauthorized: Invalid API-KEY\n").into_response();
}
if let Some(member) = NODE_STATUS.get() {
if let Some(mbr) = member.as_ref() {
match mbr.as_ref() {
"worker" => {
info!("Accepted new config from master server {}", addr.ip());
let strcontent = content.as_str();
let parsed = noyalib::from_str::<Config>(strcontent);
match parsed {
Ok(_) => {
tokio::spawn(async move { apply_config(content.as_str(), st, true).await });
return (StatusCode::OK, "Accepted! Applying in background\n").into_response();
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
return (StatusCode::BAD_GATEWAY, format!("Failed: {}\n", err)).into_response();
}
}
}
_ => {
return (StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response();
}
}
}
return (StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response();
}
(StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response()
}
async fn apply_config(content: &str, st: AppState, save: bool) { async fn apply_config(content: &str, st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await; let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 { if let Some(serverlist) = sl.0 {
@@ -188,17 +242,13 @@ async fn metrics() -> impl IntoResponse {
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
let mut buffer = Vec::new(); let mut buffer = Vec::new();
if let Err(e) = encoder.encode(&metric_families, &mut buffer) { if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
// encoding error fallback return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to encode metrics: {}", e)).into_response();
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to encode metrics: {}", e)))
.unwrap();
} }
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", encoder.format_type()) .header("Content-Type", encoder.format_type())
.body(Body::from(buffer)) .body(Body::from(buffer))
.unwrap() .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response())
} }
#[allow(clippy::needless_return)] #[allow(clippy::needless_return)]
@@ -209,7 +259,7 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Body::from(format!("{}", r))) .body(Body::from(format!("{}", r)))
.unwrap(); .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
} }
if params.contains_key("all") { if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams); let resp = upstreams_to_json(&st.current_upstreams);
@@ -219,29 +269,17 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.body(Body::from(j)) .body(Body::from(j))
.unwrap() .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to get status: {}", e)))
.unwrap();
} }
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Internal Server Error {}\n", e)).into_response(),
} }
} }
Response::builder() (StatusCode::INTERNAL_SERVER_ERROR, "Parameter mismatch\n").into_response()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Parameter mismatch"))
.unwrap()
} }
pub async fn port_is_available(name: &str, address: &str) -> TcpListener { pub async fn port_is_available(name: &str, address: &str) -> TcpListener {
let addr = SocketAddr::from_str(address).unwrap_or_else(|e| panic!("{}: Invalid address format: {:?}", name, e)); let addr = SocketAddr::from_str(address).unwrap_or_else(|e| panic!("{}: Invalid address format: {:?}", name, e));
let t = Duration::from_secs(2); let t = Duration::from_secs(2);
//if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) {
// addr.set_ip(IpAddr::V4(Ipv4Addr::LOCALHOST));
//}
let p = addr.port(); let p = addr.port();
loop { loop {
match TcpListener::bind(addr).await { match TcpListener::bind(addr).await {
+93
View File
@@ -0,0 +1,93 @@
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{ServiceDiscovery, list_to_upstreams};
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::tools::Die;
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::env;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "ServicePort")]
pub port: u16,
}
pub struct ConsulDiscovery;
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or_else(|_| "0".to_string())
)]
});
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let consul_data = servers.get(num).die("Reading consul data").to_string();
let ss = format!("{}/v1/catalog/service/", consul_data);
let dc = format!("{}/v1/catalog/services?filter=%22aralez.service=yes%22+in+ServiceTags", consul_data);
if let Some(infolist) = httpclient::for_consul_list(dc.as_str(), consul.token.clone()).await {
for (k, v) in infolist {
let client_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_header_list = Vec::new();
build_headers(&v.client_headers, config.as_ref(), &mut client_header_list);
let sever_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut server_header_list = Vec::new();
build_headers(&v.server_headers, config.as_ref(), &mut server_header_list);
if !client_header_list.is_empty() {
let path_key = v.path.clone();
client_header_map.insert(Arc::from(path_key), client_header_list);
config.client_headers.insert(Arc::from(v.host.clone()), client_header_map);
}
if !server_header_list.is_empty() {
let path_key = v.path.clone();
sever_header_map.insert(Arc::from(path_key), server_header_list);
config.server_headers.insert(Arc::from(v.host.clone()), sever_header_map);
}
let pref = format!("{}{}", ss, k);
let gsm = &GlobalServiceMapping {
upstream: k.clone(),
hostname: v.host.clone(),
path: Some(v.path),
to_https: v.to_https,
redirect_to: v.redirect,
authorization: v.auth,
rate_limit: v.rate,
x4xx_limit: v.xrate,
client_headers: v.client_headers,
server_headers: v.server_headers,
};
let list = httpclient::for_consul(pref.as_str(), consul.token.clone(), gsm).await;
list_to_upstreams(list, &upstreams, gsm);
}
}
}
if let Some(lt) = kuberconsul::clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
sleep(Duration::from_secs(5)).await;
}
}
}
@@ -1,7 +1,9 @@
use crate::utils::filewatch; use crate::core::webserver;
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery}; use crate::ingress::consul::ConsulDiscovery;
use crate::utils::structs::{Configuration, UpstreamsDashMap}; use crate::ingress::kuberconsul::ServiceDiscovery;
use crate::web::webserver; use crate::ingress::kubernetes::KubernetesDiscovery;
use crate::utils::types::{Configuration, UpstreamsDashMap};
use crate::utils::watch;
use async_trait::async_trait; use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
@@ -10,6 +12,7 @@ pub struct APIUpstreamProvider {
pub config_api_enabled: bool, pub config_api_enabled: bool,
pub address: String, pub address: String,
pub masterkey: Option<String>, pub masterkey: Option<String>,
pub acme_dns_provider: Option<String>,
pub certs_dir: String, pub certs_dir: String,
pub config_dir: String, pub config_dir: String,
pub upstreams_file: String, pub upstreams_file: String,
@@ -46,7 +49,7 @@ impl Discovery for APIUpstreamProvider {
#[async_trait] #[async_trait]
impl Discovery for FromFileProvider { impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) { async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx)); tokio::spawn(watch::file_watch(self.path.clone(), tx));
} }
} }
+63
View File
@@ -0,0 +1,63 @@
use crate::utils::tools::{Die, clone_dashmap_into, compare_dashmaps, print_upstreams};
use crate::utils::types::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
#[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
if let Some(list) = lt {
let key: Arc<str> = Arc::from(i.hostname.as_str());
match upstreams.get(&key) {
Some(upstr) => {
for (k, v) in list {
upstr.value().insert(k, v);
}
}
None => {
upstreams.insert(key, list);
}
};
}
}
pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
let path = conf.path.as_deref().unwrap_or("/");
upstreams.insert(Arc::from(path), (values, AtomicUsize::new(0)));
}
pub async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.die("Reading kubernetes token");
let mut contents = String::new();
file.read_to_string(&mut contents).await.die("Kubernetes token to string");
contents.trim().to_string()
}
#[async_trait]
pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
}
pub async fn clone_compare(upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(upstreams, &config.upstreams) {
let tosend = Configuration {
upstreams: Default::default(),
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(upstreams, &config.upstreams);
clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend);
}
None
}
+165
View File
@@ -0,0 +1,165 @@
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use log::error;
use rand::RngExt;
use std::env;
use std::sync::Arc;
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{ServiceDiscovery, clone_compare, list_to_upstreams};
use crate::ingress::kubewatcher::{AralezRouteUpdate, start_ingress_watcher_with_config};
use crate::utils::tools::Die;
use rustls::crypto::CryptoProvider;
use rustls::crypto::ring::default_provider;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSliceList {
#[serde(default)]
pub items: Vec<KubeEndpointSlice>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSlice {
pub ports: Option<Vec<KubePort>>,
#[serde(default)]
pub endpoints: Vec<KubeEndpoint>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpoint {
#[serde(default)]
pub addresses: Vec<String>,
pub conditions: Option<KubeConditions>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeConditions {
pub ready: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubePort {
pub port: Option<u16>,
}
pub struct KubernetesDiscovery;
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or_else(|_| "0".to_string())
)]
});
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).die("Getting k8s api server").to_string();
let path = kuber.tokenpath.unwrap_or_else(|| "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let token = kuberconsul::read_token(path.as_str()).await;
if CryptoProvider::get_default().is_none() {
default_provider().install_default().die("Failed to install rustls crypto provider");
}
let (tx, mut rx) = tokio::sync::mpsc::channel::<AralezRouteUpdate>(100);
let srv = server.clone();
let tok = token.clone();
tokio::spawn(async move {
if let Err(e) = start_ingress_watcher_with_config(srv.as_str(), tok.as_str(), tx, "aralez").await {
error!("Fatal watcher error: {:?}", e);
}
});
let upstreams = UpstreamsDashMap::new();
while let Some(update) = rx.recv().await {
let host_key: Arc<str> = Arc::from(update.host.as_str());
if update.is_deleted {
upstreams.remove(&host_key);
config.client_headers.remove(&host_key);
} else {
let service = GlobalServiceMapping {
upstream: update.service_name.clone(),
hostname: update.host.clone(),
path: Some(update.path.clone()),
to_https: None,
redirect_to: None,
authorization: None,
rate_limit: update.rate_limit,
x4xx_limit: update.x4xx_limit,
client_headers: None,
server_headers: None,
};
let cheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let sheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_headers_list = Vec::new();
let mut server_headers_list = Vec::new();
let mut chl: Option<Vec<String>> = None;
let mut shl: Option<Vec<String>> = None;
if let Some(hdr) = service.client_headers.as_ref() {
chl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(hdr) = service.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(ch) = update.client_headers {
chl.get_or_insert_with(Vec::new).extend(ch);
}
if let Some(hdr) = update.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
build_headers(&chl, config.as_ref(), &mut client_headers_list);
if !client_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
cheader_list.insert(Arc::from(path_key), client_headers_list);
config.client_headers.insert(host_key.clone(), cheader_list);
}
build_headers(&shl, config.as_ref(), &mut server_headers_list);
if !server_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
sheader_list.insert(Arc::from(path_key), server_headers_list);
config.server_headers.insert(host_key.clone(), sheader_list);
}
let url = format!(
"https://{}/apis/discovery.k8s.io/v1/namespaces/{}/endpointslices?labelSelector=kubernetes.io%2Fservice-name%3D{}",
server, update.namespace, update.service_name
);
let list = httpclient::for_kuber(&url, &token, &service).await;
if list.is_none() {
error!("Endpoint URL: {} returned empty/invalid response", url);
}
list_to_upstreams(list, &upstreams, &service);
}
if let Some(lt) = clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
}
}
}
}
+279
View File
@@ -0,0 +1,279 @@
use dashmap::DashMap;
use futures::StreamExt;
use hyper::Uri;
use k8s_openapi::api::core::v1::{Node, Service};
use k8s_openapi::api::networking::v1::Ingress;
use kube::api::{Patch, PatchParams};
use kube::{
api::Api,
client::Client,
config::{AuthInfo, Config},
runtime::watcher::{self, Event},
};
use log::{error, info};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub struct AralezRouteUpdate {
pub host: String,
pub path: String,
pub namespace: String,
pub service_name: String,
pub is_deleted: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
struct AralezAnnotation {
rate_limit: Option<isize>,
x4xx_limit: Option<u32>,
namespace: String,
client_headers: Option<Vec<String>>,
server_headers: Option<Vec<String>>,
}
lazy_static::lazy_static! {
static ref PATCHED_INGRESSES: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
}
pub async fn start_ingress_watcher_with_config(
api_server: &str,
token: &str,
tx: mpsc::Sender<AralezRouteUpdate>,
target_class: &'static str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = create_custom_k8s_client(api_server, token, true).await?;
let ingresses: Api<Ingress> = Api::all(client.clone());
let watcher_config = watcher::Config::default().any_semantic();
let mut stream = watcher::watcher(ingresses, watcher_config).boxed();
while let Some(event) = stream.next().await {
match event {
Ok(Event::Apply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::Delete(ing)) => {
process_ingress_change(&client, &ing, target_class, true, &tx).await;
}
Ok(Event::InitApply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::InitDone) => {
info!("Ingress watcher initial sync complete");
}
Err(err) => {
error!("Kubernetes Ingress watch error: {:?}", err);
}
_ => {}
}
}
Ok(())
}
async fn process_ingress_change(client: &Client, ing: &Ingress, target_class: &str, is_deleted: bool, tx: &mpsc::Sender<AralezRouteUpdate>) {
let spec = match &ing.spec {
Some(s) => s,
None => return,
};
let class_name = spec.ingress_class_name.as_deref().unwrap_or_default();
if class_name != target_class {
return;
}
if !is_deleted {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
let key = format!("{}/{}", ns, name);
let external_ip = resolve_external_ip(client).await;
let status_already_correct = ing
.status
.as_ref()
.and_then(|s| s.load_balancer.as_ref())
.and_then(|lb| lb.ingress.as_ref())
.map(|list| list.iter().any(|i| i.ip.as_deref() == Some(&external_ip)))
.unwrap_or(false);
if status_already_correct {
PATCHED_INGRESSES.insert(key, external_ip);
} else {
PATCHED_INGRESSES.insert(key.clone(), external_ip.clone());
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let key_clone = key.clone();
tokio::spawn(async move {
if let Err(e) = patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, &external_ip).await {
error!("Failed to patch ingress status address {}/{}: {}", ns_clone, name_clone, e);
PATCHED_INGRESSES.remove(&key_clone);
}
});
}
}
} else {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
PATCHED_INGRESSES.remove(&format!("{}/{}", ns, name));
}
}
if let Some(rules) = &spec.rules {
for rule in rules {
let host = rule.host.clone().unwrap_or_else(|| "*".to_string());
if let Some(http) = &rule.http {
for path_entry in &http.paths {
let path = path_entry.path.clone().unwrap_or_else(|| "/".to_string());
if let Some(service) = &path_entry.backend.service {
let service_name = service.name.clone();
let annot = parse_ingress_config(ing);
let update = AralezRouteUpdate {
host: host.clone(),
path,
service_name,
is_deleted,
rate_limit: annot.rate_limit,
x4xx_limit: annot.x4xx_limit,
client_headers: annot.client_headers,
server_headers: annot.server_headers,
namespace: annot.namespace,
};
if let Err(e) = tx.send(update).await {
error!("Failed to send route update to channel: {:?}", e);
}
}
}
}
}
}
}
pub async fn create_custom_k8s_client(api_server_url: &str, bearer_token: &str, insecure_tls: bool) -> Result<Client, Box<dyn std::error::Error>> {
let full_url = if !api_server_url.starts_with("http://") && !api_server_url.starts_with("https://") {
format!("https://{}", api_server_url)
} else {
api_server_url.to_string()
};
let uri: Uri = full_url.parse()?;
let mut config = Config::new(uri);
config.auth_info = AuthInfo {
token: Some(bearer_token.trim().to_string().into()),
..Default::default()
};
config.connect_timeout = Some(Duration::from_secs(10));
config.read_timeout = None;
if insecure_tls {
config.accept_invalid_certs = true;
}
let client = Client::try_from(config)?;
Ok(client)
}
fn parse_ingress_config(ing: &Ingress) -> AralezAnnotation {
let namespace = ing.metadata.namespace.clone().unwrap_or_else(|| "default".to_string());
let annotations = ing.metadata.annotations.as_ref();
let client_headers = annotations.and_then(|a| a.get("aralez.rs/client_headers")).and_then(|ch| serde_json::from_str(ch).ok());
let server_headers = annotations.and_then(|a| a.get("aralez.rs/server_headers")).and_then(|sh| serde_json::from_str(sh).ok());
AralezAnnotation {
rate_limit: annotations.and_then(|a| a.get("aralez.rs/rate_limit")).and_then(|v| v.parse().ok()),
x4xx_limit: annotations.and_then(|a| a.get("aralez.rs/x4xx_limit")).and_then(|v| v.parse().ok()),
client_headers,
server_headers,
namespace,
}
}
pub async fn patch_ingress_status_address(client: &Client, namespace: &str, name: &str, external_ip: &str) -> Result<(), kube::Error> {
let ingresses: Api<Ingress> = Api::namespaced(client.clone(), namespace);
let patch = json!({
"status": {
"loadBalancer": {
"ingress": [
{
"ip": external_ip
}
]
}
}
});
let params = PatchParams::default();
match ingresses.patch_status(name, &params, &Patch::Strategic(patch)).await {
Ok(_) => {
info!("Successfully patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip);
Ok(())
}
Err(e) => {
error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, e);
Err(e)
}
}
}
pub async fn discover_node_ip(client: &Client) -> Option<String> {
let nodes: Api<Node> = Api::all(client.clone());
if let Ok(node_list) = nodes.list(&Default::default()).await {
for node in node_list {
if let Some(status) = node.status {
if let Some(addresses) = status.addresses {
for addr in &addresses {
if addr.type_ == "ExternalIP" {
return Some(addr.address.clone());
}
}
for addr in &addresses {
if addr.type_ == "InternalIP" {
return Some(addr.address.clone());
}
}
}
}
}
}
None
}
pub async fn discover_controller_ip(client: &Client, namespace: &str, service_name: &str) -> Option<String> {
let services: Api<Service> = Api::namespaced(client.clone(), namespace);
if let Ok(svc) = services.get(service_name).await {
if let Some(status) = svc.status {
if let Some(lb) = status.load_balancer {
if let Some(ingress_list) = lb.ingress {
for ing in ingress_list {
if let Some(ip) = ing.ip {
return Some(ip);
}
if let Some(hostname) = ing.hostname {
return Some(hostname);
}
}
}
}
}
}
None
}
pub async fn resolve_external_ip(client: &Client) -> String {
if let Ok(ip) = std::env::var("ARALEZ_EXTERNAL_IP") {
let trimmed = ip.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let svc_ns = std::env::var("ARALEZ_SERVICE_NAMESPACE").unwrap_or_else(|_| "aralez".to_string());
if let Some(ip) = discover_controller_ip(client, &svc_ns, &svc_name).await {
return ip;
}
if let Some(ip) = discover_node_ip(client).await {
return ip;
}
"127.0.0.1".to_string()
}
+7
View File
@@ -0,0 +1,7 @@
pub mod consul;
pub mod discovery;
pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
pub use discovery::*;
+7
View File
@@ -0,0 +1,7 @@
pub mod auth;
pub mod cache;
pub mod core;
pub mod ingress;
pub mod logging;
pub mod tls;
pub mod utils;
+337
View File
@@ -0,0 +1,337 @@
use crate::logging::types::start_logging_backend;
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::tools::Die;
use crate::utils::types::AppConfig;
use anyhow::Result;
use log::{Level, LevelFilter, Record, error, info, log};
use log4rs::append::Append;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::config::Logger;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use serde::Serialize;
use std::cell::RefCell;
use std::fmt::Write;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
#[derive(Debug, Serialize, Clone)]
pub struct StructuredSystemLog {
pub target: &'static str,
pub level: Level,
pub message: String,
}
thread_local! {
static LOG_BUF: RefCell<String> = RefCell::new(String::with_capacity(512));
}
static ACCESS_LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
pub(crate) static SYSTEM_LOG_SENDER: OnceLock<mpsc::Sender<StructuredSystemLog>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
static IS_STRUCTURED: OnceLock<bool> = OnceLock::new();
static LOG_BACKEND: OnceLock<&'static str> = OnceLock::new();
pub fn set_backend(key: String) -> Result<(), &'static str> {
let static_key: &'static str = Box::leak(key.into_boxed_str());
LOG_BACKEND.set(static_key).map_err(|_| "LOG_BACKEND was already initialized!")
}
pub fn get_backend() -> &'static str {
LOG_BACKEND.get().copied().unwrap_or("system")
}
#[derive(Debug)]
pub struct StructuredChannelAppender {
sender: mpsc::Sender<StructuredSystemLog>,
}
impl StructuredChannelAppender {
pub fn new(sender: mpsc::Sender<StructuredSystemLog>) -> Self {
Self { sender }
}
}
impl Append for StructuredChannelAppender {
fn append(&self, record: &Record) -> Result<()> {
let message = LOG_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
let _ = write!(&mut *b, "{}", record.args());
b.clone()
});
let target: &'static str = unsafe { std::mem::transmute(record.target()) };
let msg = StructuredSystemLog {
target,
level: record.level(),
message,
};
let _ = self.sender.try_send(msg);
Ok(())
}
fn flush(&self) {}
}
pub async fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let mut pat: String = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m} {n}".to_string();
if let Some(ptrn) = conf.log_pattern.clone() {
pat = ptrn;
}
let pattern = pat.as_str();
if let Some(backend) = conf.log_structured.as_deref() {
if let Some(backend_name) = backend.split_whitespace().next() {
let _ = set_backend(backend_name.to_string());
init_structured_log().await;
}
let syslog_appender: Option<Box<dyn Append>> = SYSTEM_LOG_SENDER
.get()
.cloned()
.map(|sender| Box::new(StructuredChannelAppender::new(sender)) as Box<dyn Append>);
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let mut config_builder = Log4rsConfig::builder().appender(Appender::builder().build("stdout", Box::new(stdout)));
if let Some(appender) = syslog_appender {
config_builder = config_builder
.appender(Appender::builder().build("syslog_appender", appender))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_proxy", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_core", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_pool", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_cache", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("hyper_util", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("aralez", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("h2", log_level));
} else {
config_builder = config_builder
.logger(Logger::builder().build("pingora_core", LevelFilter::Off))
.logger(Logger::builder().build("pingora_pool", LevelFilter::Off))
.logger(Logger::builder().build("pingora_cache", LevelFilter::Off))
.logger(Logger::builder().build("hyper_util", LevelFilter::Off))
.logger(Logger::builder().build("aralez", LevelFilter::Off))
.logger(Logger::builder().build("h2", LevelFilter::Off));
}
let config = config_builder.build(Root::builder().appender("stdout").build(log_level)).die("Logging config builder");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Enabling structured logging with backend : {}", backend);
let _ = IS_STRUCTURED.set(true);
return;
}
if let Some(location) = location {
let parts: Vec<&str> = location.split(',').map(|s| s.trim()).collect();
let path = match parts.first().filter(|s| !s.is_empty()) {
Some(p) => p,
None => {
error!("Invalid log location string provided; falling back to stdout");
return;
}
};
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).die("Logging roller builder");
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = match RollingFileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(path, Box::new(policy)) {
Ok(f) => f,
Err(e) => {
error!("Failed to create log file at {}: {}. Falling back to stdout.", path, e);
return;
}
};
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("file").build(log_level))
.die("Parsing Lo4rsConfig");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("stdout").build(log_level))
.die("Parsing Lo4rsConfig");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("No files are configured, logging to stdout");
}
}
pub async fn init_access_log(level_str: &str) {
let level = LogLevel::parse(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn parse(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
#[derive(Debug)]
pub enum MatchStatus {
Ok2xx,
Er4xx,
Er5xx,
}
impl MatchStatus {
#[inline]
pub fn from_code(code: u16) -> Self {
match code {
100..=399 => Self::Ok2xx,
400..=499 => Self::Er4xx,
_ => Self::Er5xx,
}
}
}
pub async fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let status = MatchStatus::from_code(response_code);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => matches!(status, MatchStatus::Er5xx),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
let msg = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if IS_STRUCTURED.get().is_some() {
write_access_log(&msg).await;
return;
}
if let Some(sender) = ACCESS_LOG_SENDER.get() {
if sender.try_send(msg).is_err() {
LOGGING_ERRORS.inc();
}
}
}
pub async fn init_access_logging(enabled: Option<String>) {
if enabled.is_some() {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
let _ = ACCESS_LOG_SENDER.set(ltx);
tokio::spawn(async move { access_log_receiver(lrx).await });
}
}
pub async fn init_structured_log() {
let (tx, rx) = mpsc::channel::<StructuredSystemLog>(LOG_BUFFER);
let _ = SYSTEM_LOG_SENDER.set(tx);
let backend = get_backend();
start_logging_backend(backend, rx);
}
pub async fn access_log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.recv().await {
write_access_log(&msg).await;
}
}
async fn write_access_log(msg: &LogMessage) {
let level = match MatchStatus::from_code(msg.response_code) {
MatchStatus::Ok2xx => Level::Info,
MatchStatus::Er4xx => Level::Warn,
MatchStatus::Er5xx => Level::Error,
};
log!(
level,
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
+187
View File
@@ -0,0 +1,187 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use crate::utils::tools::get_hostname;
use async_trait::async_trait;
use bytes::{BufMut, BytesMut};
use elasticsearch::auth::Credentials;
use elasticsearch::http::transport::Transport;
use elasticsearch::{BulkParts, Elasticsearch};
use humantime::format_rfc3339;
use serde_json::json;
use std::env;
use std::sync::LazyLock;
use std::time::{Duration, SystemTime};
use tokio::sync::mpsc;
use tokio::sync::OnceCell;
static CLIENT: OnceCell<Elasticsearch> = OnceCell::const_new();
#[derive(Debug)]
struct ElasticHost {
url: &'static str,
}
struct ElasticSearchConfig {
pub hosts: &'static [ElasticHost],
pub user: &'static str,
pub password: &'static str,
pub hostname: &'static str,
pub index_name: &'static str,
pub flush_duration: u64,
pub batch_len: usize,
pub buffer_size: usize,
}
static ELASTIC: LazyLock<ElasticSearchConfig> = LazyLock::new(ElasticSearchConfig::load_from_env);
impl ElasticSearchConfig {
pub fn load_from_env() -> Self {
let raw_hosts = env::var("LOG_ELASTIC_HOSTS").unwrap_or_else(|_| "http://127.0.0.1:9200".to_string());
let mut hosts_struct = Vec::new();
for entry in raw_hosts.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (scheme, rest) = if let Some(stripped) = entry.strip_prefix("https://") {
("https", stripped)
} else if let Some(stripped) = entry.strip_prefix("http://") {
("http", stripped)
} else {
("http", entry)
};
let (address, port) = match rest.split_once(':') {
Some((host_part, port_part)) => {
let port = port_part.parse::<u16>().unwrap_or(9200);
(host_part, port)
}
None => (rest, 9200),
};
let full_url = format!("{}://{}:{}", scheme, address, port);
hosts_struct.push(ElasticHost {
url: Box::leak(full_url.into_boxed_str()),
});
}
let user = env::var("LOG_ELASTIC_USER").unwrap_or_else(|_| "elastic".to_string());
let password = env::var("LOG_ELASTIC_PASSWORD").unwrap_or_else(|_| "elastic".to_string());
let index_name = env::var("LOG_ELASTIC_INDEX").unwrap_or_else(|_| "aralez-logs".to_string());
let s_flush_duration = env::var("LOG_ELASTIC_FLUSH_DURATION").unwrap_or_else(|_| "2".to_string());
let flush_duration = s_flush_duration.parse::<u64>().unwrap_or(2);
let s_batch_len = env::var("LOG_ELASTIC_BATCH_LEN").unwrap_or_else(|_| "200".to_string());
let batch_len = s_batch_len.parse::<usize>().unwrap_or(200);
let buffer_size: usize = 100 * 512;
Self {
hosts: Box::leak(hosts_struct.into_boxed_slice()),
user: Box::leak(user.into_boxed_str()),
password: Box::leak(password.into_boxed_str()),
flush_duration,
batch_len,
buffer_size,
hostname: Box::leak(get_hostname().into_boxed_str()),
index_name: Box::leak(index_name.into_boxed_str()),
}
}
}
async fn make_es_pool() -> Elasticsearch {
let valid_urls: Vec<&str> = ELASTIC.hosts.iter().map(|target| target.url).collect();
if valid_urls.is_empty() {
log::error!("No valid Elasticsearch host URLs found in configuration");
return Elasticsearch::default();
}
let transport = match Transport::sniffing_node_list(valid_urls, Duration::from_secs(180)) {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create sniffing transport: {}", e);
return Elasticsearch::default();
}
};
let credentials = Credentials::Basic(ELASTIC.user.to_string(), ELASTIC.password.to_string());
transport.set_auth(credentials);
let client = Elasticsearch::new(transport);
if let Ok(res) = client.ping().send().await {
if res.status_code().is_success() {
log::info!("Elasticsearch sniffing pool initialized successfully");
return client;
}
}
log::warn!("Elasticsearch pool initialized, but cluster ping returned non-200");
client
}
struct ElasticSearch;
#[async_trait]
impl WriteLog for ElasticSearch {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
let mut batch = Vec::with_capacity(ELASTIC.batch_len);
let mut timer = tokio::time::interval(Duration::from_secs(ELASTIC.flush_duration));
let mut buffer = BytesMut::with_capacity(ELASTIC.buffer_size);
let index_header = format!("{{\"index\":{{\"_index\":\"{}\"}}}}\n", ELASTIC.index_name).into_bytes();
loop {
tokio::select! {
Some(msg) = rx.recv() => {
batch.push(msg);
if batch.len() >= ELASTIC.batch_len {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
_ = timer.tick() => {
if !batch.is_empty() {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
}
}
}
}
async fn flush_to_es(batch: &mut Vec<StructuredSystemLog>, buffer: &mut BytesMut, index_header: &[u8], hostname: &str) {
buffer.clear();
for msg in batch.drain(..) {
buffer.extend_from_slice(index_header);
let logstash_payload = json!({
"@timestamp": format_rfc3339(SystemTime::now()).to_string(),
"@version": "1",
"log": {
"level": msg.level,
"logger": msg.target,
},
"message": msg.message,
"host": {
"hostname": hostname,
}
});
if serde_json::to_writer((buffer).writer(), &logstash_payload).is_ok() {
buffer.extend_from_slice(b"\n");
}
}
if buffer.is_empty() {
return;
}
let payload = buffer.split().freeze();
let client = CLIENT.get_or_init(make_es_pool).await;
let _ = client.bulk(BulkParts::None).body(vec![payload]).send().await;
}
inventory::submit! {
LogBackendPlugin {
name: "elasticsearch",
factory: || Box::new(ElasticSearch),
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use async_trait::async_trait;
use tokio::sync::mpsc;
pub struct Example;
#[async_trait]
impl WriteLog for Example {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
while let Some(msg) = rx.recv().await {
println!("Sending log Example : {:?}", msg);
}
}
}
inventory::submit! {
LogBackendPlugin {
name: "example",
factory: || Box::new(Example),
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod core;
pub mod elasticsearch;
pub mod example;
pub mod types;
+36
View File
@@ -0,0 +1,36 @@
use crate::logging::core::StructuredSystemLog;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::LazyLock;
use tokio::sync::mpsc;
#[async_trait]
pub trait WriteLog: Send + Sync {
async fn run(&self, rx: mpsc::Receiver<StructuredSystemLog>);
}
pub struct LogBackendPlugin {
pub name: &'static str,
pub factory: fn() -> Box<dyn WriteLog>,
}
inventory::collect!(LogBackendPlugin);
static BACKENDS: LazyLock<HashMap<&'static str, Box<dyn WriteLog>>> = LazyLock::new(|| {
let mut map = HashMap::new();
for plugin in inventory::iter::<LogBackendPlugin> {
map.insert(plugin.name, (plugin.factory)());
}
map
});
pub fn start_logging_backend(backend: &str, rx: mpsc::Receiver<StructuredSystemLog>) {
if let Some(logger) = BACKENDS.get(backend) {
let logger_ref = logger.as_ref();
tokio::spawn(async move {
logger_ref.run(rx).await;
});
} else {
log::warn!("Unsupported logging mechanism: {}", backend);
}
}
+2 -4
View File
@@ -1,6 +1,4 @@
mod tls; use aralez::core;
mod utils;
mod web;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@@ -12,5 +10,5 @@ fn main() {
std::process::exit(0); std::process::exit(0);
} }
web::start::run(); core::start::run();
} }
-2
View File
@@ -1,2 +0,0 @@
pub mod account;
pub mod order;
+75
View File
@@ -0,0 +1,75 @@
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use log::info;
use reqwest::Client;
use serde_json::json;
pub struct CloudflareProvider {
pub client: Client,
pub api_token: String,
pub zone_id: String,
}
impl CloudflareProvider {
pub fn new() -> Self {
let api_token = std::env::var("CLOUDFLARE_API_TOKEN").die("Environment variable CLOUDFLARE_API_TOKEN is required");
let zone_id = std::env::var("CLOUDFLARE_ZONE_ID").die("Environment variable CLOUDFLARE_ZONE_ID is required");
Self {
client: Client::new(),
api_token,
zone_id,
}
}
}
#[async_trait::async_trait]
impl DnsProvider for CloudflareProvider {
async fn create_txt_record(&self, _domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records", self.zone_id);
let response = self
.client
.post(&url)
.bearer_auth(&self.api_token)
.json(&json!({
"type": "TXT",
"name": name,
"content": value,
"ttl": 120,
}))
.send()
.await?;
let res_json: serde_json::Value = response.json().await?;
if !res_json["success"].as_bool().unwrap_or(false) {
return Err(format!("Cloudflare API error: {:?}", res_json["errors"]).into());
}
let record_id = res_json["result"]["id"].as_str().ok_or("missing id")?.to_string();
info!("Created TXT record name: {}, id: {}", name, record_id);
lookup_wait(name, Some("1.1.1.1"), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", self.zone_id, record_id);
let response = self.client.delete(&url).bearer_auth(&self.api_token).send().await?;
let res_json: serde_json::Value = response.json().await?;
if !res_json["success"].as_bool().unwrap_or(false) {
return Err(format!("Cloudflare API error deleting record: {:?}", res_json["errors"]).into());
}
info!("Deleted TXT record name: {}, id: {}", record_name, record_id);
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "cloudflare",
factory: || Box::new(CloudflareProvider::new()),
}
}
+71
View File
@@ -0,0 +1,71 @@
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use async_trait::async_trait;
use log::info;
use serde_json::json;
use std::env;
pub struct DigitalOceanProvider {
token: String,
client: reqwest::Client,
}
impl DigitalOceanProvider {
pub fn new() -> Self {
let token = env::var("DIGITALOCEAN_TOKEN").die("Environment variable DIGITALOCEAN_TOKEN is required");
Self {
token,
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl DnsProvider for DigitalOceanProvider {
async fn create_txt_record(&self, domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let relative_name = name.strip_suffix(&format!(".{}", domain)).unwrap_or(name);
let url = format!("https://api.digitalocean.com/v2/domains/{}/records", domain);
let res = self
.client
.post(&url)
.bearer_auth(&self.token)
.json(&json!({
"type": "TXT",
"name": relative_name,
"data": value,
"ttl": 30
}))
.send()
.await?;
if !res.status().is_success() {
let err_text = res.text().await?;
return Err(format!("DigitalOcean API error: {}", err_text).into());
}
let json: serde_json::Value = res.json().await?;
let record_id = json["domain_record"]["id"].as_i64().ok_or("Missing record ID in DigitalOcean response")?.to_string();
info!("Created TXT record name: {}, id: {}", name, record_id);
lookup_wait(name, Some("1.1.1.1"), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, record_id: &str, domain: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.digitalocean.com/v2/domains/{}/records/{}", domain, record_id);
let res = self.client.delete(&url).bearer_auth(&self.token).send().await?;
if !res.status().is_success() {
let err_text = res.text().await?;
return Err(format!("Failed to delete DigitalOcean record: {}", err_text).into());
}
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "digitalocean",
factory: || Box::new(DigitalOceanProvider::new()),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod cloudflare;
pub mod digitalocean;
pub mod route53;
+130
View File
@@ -0,0 +1,130 @@
// ------------------------ //
// This file is vibecoded !
// ------------------------ //
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use aws_sdk_route53::Client;
use aws_sdk_route53::types::{Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet, RrType};
use log::info;
use tokio::runtime::Handle;
pub struct Route53Provider {
pub client: Client,
pub hosted_zone_id: String,
}
impl Route53Provider {
pub fn new() -> Self {
let hosted_zone_id = std::env::var("AWS_HOSTED_ZONE_ID").die("Environment variable AWS_HOSTED_ZONE_ID is required");
let config = if let Ok(handle) = Handle::try_current() {
// If we're inside a tokio runtime, we can block on it directly
handle.block_on(aws_config::load_from_env())
} else {
// If we're completely outside tokio, spin up a temporary current-thread runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.die("Failed to create current-thread runtime")
.block_on(aws_config::load_from_env())
};
let client = Client::new(&config);
Self { client, hosted_zone_id }
}
pub async fn get_authoritative_ns_ip(&self) -> Option<String> {
// Query Route53 for the zone's details (includes assigned Name Servers)
let response = self.client.get_hosted_zone().id(&self.hosted_zone_id).send().await.ok()?;
// Pick the first name server assigned to this zone (e.g., "ns-1289.awsdns-29.org")
let ns_domain = response.delegation_set()?.name_servers().first()?;
// Resolve that nameserver's domain name to an IP address
let ips = tokio::net::lookup_host(format!("{}:53", ns_domain)).await.ok()?;
let ip = ips.into_iter().next()?.ip().to_string();
info!("Targeting Route53 authoritative NS {} ({}) for polling", ns_domain, ip);
Some(ip)
}
}
#[async_trait::async_trait]
impl DnsProvider for Route53Provider {
async fn create_txt_record(&self, _domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// AWS Route53 requires TXT strings to be wrapped in double quotes
let formatted_value = format!("\"{}\"", value);
let change = Change::builder()
.action(ChangeAction::Upsert)
.resource_record_set(
ResourceRecordSet::builder()
.name(name)
.r#type(RrType::Txt)
.ttl(60)
.resource_records(ResourceRecord::builder().value(formatted_value).build()?)
.build()?,
)
.build()?;
let change_batch = ChangeBatch::builder().changes(change).build()?;
let response = self
.client
.change_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.change_batch(change_batch)
.send()
.await?;
let record_id = response.change_info().ok_or("Missing ChangeInfo in Route53 response")?.id().to_string();
info!("Created Route53 TXT record name: {}, change_id: {}", name, record_id);
let target_dns_ip = self.get_authoritative_ns_ip().await;
lookup_wait(name, target_dns_ip.as_deref(), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, _record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Since Route53 requires record value context to delete, we look up or pass existing TXT record values.
// For ACME cleanup, issuing a DELETE with wildcards/upsert strategy or direct deletion:
let list_res = self
.client
.list_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.start_record_name(record_name)
.start_record_type(RrType::Txt)
.max_items(1)
.send()
.await?;
if let Some(record_set) = list_res.resource_record_sets().first() {
if record_set.name().trim_end_matches('.') == record_name.trim_end_matches('.') {
let change = Change::builder().action(ChangeAction::Delete).resource_record_set(record_set.clone()).build()?;
let change_batch = ChangeBatch::builder().changes(change).build()?;
self.client
.change_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.change_batch(change_batch)
.send()
.await?;
info!("Deleted Route53 TXT record name: {}", record_name);
return Ok(());
}
}
info!("Route53 TXT record name: {} already deleted or not found", record_name);
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "route53",
factory: || Box::new(Route53Provider::new()),
}
}
+11 -25
View File
@@ -1,64 +1,50 @@
use crate::tls::acme::order::CHALLENGES; use crate::core::webserver;
use crate::tls::acme::{account, order}; use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body; use axum::body::Body;
use axum::extract::State; use axum::extract::State;
use axum::http::{Response, StatusCode}; use axum::http::{Response, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
#[allow(clippy::needless_return)] #[allow(clippy::needless_return)]
pub async fn acme_create(State(state): State<crate::web::webserver::AppState>) -> impl IntoResponse { pub async fn acme_create(State(state): State<webserver::AppState>) -> impl IntoResponse {
match account::load_or_create(state.cert_creds.as_str()).await { match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => { Ok(txt) => {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "text/plain") .header("Content-Type", "text/plain")
.body(Body::from(txt)) .body(Body::from(txt))
.unwrap() .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
} }
Err(e) => { Err(e) => {
return Response::builder() return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create account {}\n", e)).into_response();
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
} }
}; };
} }
#[allow(clippy::needless_return)] #[allow(clippy::needless_return)]
pub async fn acme_order(State(state): State<crate::web::webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse { pub async fn acme_order(State(state): State<webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
let domain_clean = domain.trim_matches('/'); let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir).await { match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir, state.acme_dns_provider).await {
Ok(txt) => { Ok(txt) => {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "text/plain") .header("Content-Type", "text/plain")
.body(Body::from(txt)) .body(Body::from(txt))
.unwrap() .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
} }
Err(e) => { Err(e) => {
return Response::builder() return (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal Server Error: {}\n", e)).into_response();
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
} }
}; };
} }
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse { pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() { if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) { if let Some(key_authorization) = challenges.get(&token) {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
.header("Content-Type", "text/plain") .header("Content-Type", "text/plain")
.body(Body::from(key_authorization.clone())) .body(Body::from(key_authorization.clone()))
.unwrap(); .unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
} }
} }
Response::builder() (StatusCode::NOT_FOUND, "Not found\n").into_response()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "text/plain")
.body(Body::from("Not found"))
.unwrap()
} }
+46
View File
@@ -0,0 +1,46 @@
use crate::utils::tools::Die;
use hickory_resolver::TokioResolver;
use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts};
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::proto::rr::RData;
use log::info;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
pub async fn lookup_wait(record_name: &str, dns_server_ip: Option<&str>, max_wait_secs: u64, expected_value: &str) -> bool {
let target_ip = dns_server_ip.and_then(|ip| IpAddr::from_str(ip).ok()).unwrap_or_else(|| IpAddr::from([1, 1, 1, 1]));
let name_server = NameServerConfig::udp(target_ip);
let config = ResolverConfig::from_name_servers(vec![name_server]);
let mut opts = ResolverOpts::default();
opts.cache_size = 0;
let resolver = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(opts)
.build()
.die("failed to build DNS resolver");
let start = std::time::Instant::now();
let poll_interval = Duration::from_secs(3);
while start.elapsed().as_secs() < max_wait_secs {
if let Ok(txt_lookup) = resolver.txt_lookup(record_name).await {
for record in txt_lookup.answers() {
if let RData::TXT(txt) = &record.data {
for data in &txt.txt_data {
if let Ok(txt_str) = std::str::from_utf8(data) {
if txt_str == expected_value {
tokio::time::sleep(Duration::from_secs(5)).await;
info!("DNS record propagated successfully for {} : {}", record_name, txt_str);
return true;
}
}
}
}
}
} else {
info!("Waiting for TXT record {}", record_name);
}
tokio::time::sleep(poll_interval).await;
}
false
}
+9
View File
@@ -0,0 +1,9 @@
pub mod account;
pub mod dns;
pub mod http01;
pub mod lookup;
pub mod order;
pub mod types;
pub use account::*;
pub use order::*;
+53 -24
View File
@@ -1,39 +1,38 @@
use crate::tls::acme::account::get_account; use crate::tls::acme::account::get_account;
use crate::utils::parceyaml::DOMAINS; use crate::tls::acme::types::get_provider;
use crate::utils::lazylock::CHALLENGES;
use crate::utils::lazylock::DOMAINS;
use crate::utils::tools::Die;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy}; use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info}; use log::{error, info};
use pingora::prelude::sleep; use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair}; use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap; use sha2::{Digest, Sha256};
use std::fs; use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration; use std::time::Duration;
use x509_parser::prelude::*; use x509_parser::prelude::*;
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new())); pub async fn refresh_order(certs_dir: String, autoconf_dir: String, provider: Option<String>) {
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json"; let credsfile = autoconf_dir + "/acme_credentials.json";
loop { loop {
for item in DOMAINS.iter() { for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone()).await; let _what = order(item.key(), credsfile.as_str(), certs_dir.clone(), provider.clone()).await;
} }
sleep(Duration::from_secs(12 * 3600)).await; sleep(Duration::from_secs(12 * 3600)).await;
} }
} }
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> { pub async fn order(domain: &str, credsfile: &str, certs_dir: String, provider: Option<String>) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt"; let crt = format!("{}/{}.crt", certs_dir, domain);
let key = certs_dir.clone() + "/" + domain + ".key"; let key = format!("{}/{}.key", certs_dir, domain);
if DOMAINS.get(domain).is_none() { if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true); DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new(); let newlist: Vec<String> = DOMAINS.iter().map(|item| item.key().to_string()).collect();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) { if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json"); let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) { if let Err(err) = fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err); error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err)); return Err(Box::from(err));
} }
@@ -43,7 +42,6 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
if let Ok(expiry) = cert_expiry(crt.as_str()) { if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs(); let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 { if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string()); return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
} }
}; };
@@ -52,14 +50,43 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?; let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations(); let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await { while let Some(auth) = authorizations.next().await {
let mut auth = auth?; let mut auth = auth?;
let mut challenge_handle = auth.challenge(ChallengeType::Http01).ok_or("no http01 challenge found")?; let domain_name = auth.identifier().to_string();
let (challenge_type, is_dns) = match &provider {
Some(_) => (ChallengeType::Dns01, true),
None => (ChallengeType::Http01, false),
};
let mut challenge_handle = auth.challenge(challenge_type).ok_or("Requested challenge type not offered by ACME server")?;
let key_auth = challenge_handle.key_authorization(); let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string(); let key_auth_str = key_auth.as_str().to_string();
let token = key_auth_str.split('.').next().ok_or("invalid key authorization")?.to_string();
CHALLENGES.write().unwrap().insert(token, key_auth_str); if is_dns {
challenge_handle.set_ready().await?; let prov = provider.as_ref().die("Failed to get DNS provider");
let dns_provider = get_provider(prov.clone());
let dns_value = calculate_dns_value(&key_auth_str);
let clean_domain = domain_name.trim_start_matches("*.").to_string();
let record_name = format!("_acme-challenge.{}", clean_domain);
match dns_provider.create_txt_record(&clean_domain, &record_name, &dns_value).await {
Ok(record_id) => {
let ready_res = challenge_handle.set_ready().await;
let _ = dns_provider.delete_txt_record(&record_id, &record_name).await;
ready_res?;
}
Err(e) => {
eprintln!("Failed to create DNS record: {}", e);
return Err(e);
}
}
} else {
let token = key_auth_str.split('.').next().ok_or("Invalid key authorization")?.to_string();
CHALLENGES.write().die("Failed to write challenge").insert(token, key_auth_str);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
challenge_handle.set_ready().await?;
}
} }
let status = order.poll_ready(&RetryPolicy::default()).await?; let status = order.poll_ready(&RetryPolicy::default()).await?;
@@ -71,12 +98,9 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
let signing_request = params.serialize_request(&private_key)?; let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der(); let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?; order.finalize_csr(csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?; let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear(); CHALLENGES.write().die("Challenges lock poisoned").clear();
let private_key_pem = private_key.serialize_pem(); let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?; fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?; fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string()) Ok("Certificate is successfully generated \n".to_string())
@@ -89,3 +113,8 @@ fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let expiry = cert.validity().not_after.timestamp() as u64; let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry) Ok(expiry)
} }
fn calculate_dns_value(key_auth: &str) -> String {
let hash = Sha256::digest(key_auth.as_bytes());
URL_SAFE_NO_PAD.encode(hash)
}
+32
View File
@@ -0,0 +1,32 @@
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::LazyLock;
pub struct DnsBackendPlugin {
pub name: &'static str,
pub factory: fn() -> Box<dyn DnsProvider>,
}
inventory::collect!(DnsBackendPlugin);
static FACTORIES: LazyLock<HashMap<&'static str, fn() -> Box<dyn DnsProvider>>> = LazyLock::new(|| {
let mut map = HashMap::new();
for plugin in inventory::iter::<DnsBackendPlugin> {
map.insert(plugin.name, plugin.factory);
}
map
});
#[async_trait]
pub trait DnsProvider: Send + Sync {
async fn create_txt_record(&self, domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
async fn delete_txt_record(&self, record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
pub fn get_provider(provider: String) -> Box<dyn DnsProvider> {
FACTORIES
.get(provider.as_str())
.map(|factory| factory())
.unwrap_or_else(|| panic!("Unknown DNS provider '{}'. Available providers: {:?}", provider, FACTORIES.keys().collect::<Vec<_>>()))
}
+2 -2
View File
@@ -22,7 +22,7 @@ pub enum TlsGrade {
} }
impl TlsGrade { impl TlsGrade {
pub fn from_str(s: &str) -> Option<Self> { pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High), "high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::Medium), "medium" => Some(TlsGrade::Medium),
@@ -39,7 +39,7 @@ pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], A
} }
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) { pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::from_str(grade); let config_grade = TlsGrade::parse(grade);
match config_grade { match config_grade {
Some(TlsGrade::High) => { Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2)); let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
+5 -3
View File
@@ -1,8 +1,9 @@
use crate::tls::grades; use crate::tls::grades;
use crate::utils::tools::Die;
use dashmap::DashMap; use dashmap::DashMap;
use log::error; use log::error;
use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef}; use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use rustls_pemfile::{read_one, Item}; use rustls_pemfile::{Item, read_one};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashSet; use std::collections::HashSet;
use std::fs::File; use std::fs::File;
@@ -10,6 +11,7 @@ use std::io::BufReader;
use x509_parser::extensions::GeneralName; use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr; use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*; use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)] #[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig { pub struct CertificateConfig {
pub cert_path: String, pub cert_path: String,
@@ -37,7 +39,7 @@ pub struct Certificates {
impl Certificates { impl Certificates {
pub fn new(configs: &Vec<CertificateConfig>, _grade: &str) -> Option<Self> { pub fn new(configs: &Vec<CertificateConfig>, _grade: &str) -> Option<Self> {
let default_cert = configs.first().expect("At least one TLS certificate required"); let default_cert = configs.first().die("At least one TLS certificate required");
let mut cert_infos = Vec::new(); let mut cert_infos = Vec::new();
let name_map: DashMap<String, SslContext> = DashMap::new(); let name_map: DashMap<String, SslContext> = DashMap::new();
for config in configs { for config in configs {
@@ -54,7 +56,7 @@ impl Certificates {
cert_infos.push(cert) cert_infos.push(cert)
} }
None => { None => {
error!("Unable to load certificate info | public: {}, private: {}", &config.cert_path, &config.key_path); error!("Unable to load certificate info | public: {}, private: {}", config.cert_path, config.key_path);
return None; return None;
} }
} }
+1
View File
@@ -1,3 +1,4 @@
pub mod acme; pub mod acme;
pub mod grades; pub mod grades;
pub mod load; pub mod load;
pub use load::*;
+298
View File
@@ -0,0 +1,298 @@
use crate::utils::hcclient::{HttpClientRequest, httpclient};
use crate::utils::lazylock::{GLOBAL_PARAMS, MASTER_KEY, WORKERS};
use crate::utils::tools::Die;
use crate::utils::types::{Consul, Extraparams, Headers, Kubernetes, UpstreamsDashMap};
use bytes::Bytes;
use log::{error, info, warn};
use noyalib::to_string;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::Deref;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpstreamConfigDto {
pub paths: HashMap<String, PathConfigDto>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PathConfigDto {
pub is_http2: bool,
pub healthcheck: bool,
pub is_ssl: bool,
pub to_https: bool,
pub rate_limit: isize,
pub x4xx_limit: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<HashMap<String, String>>,
pub servers: Vec<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub client_headers: Option<Vec<String>>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub server_headers: Option<Vec<String>>,
}
#[derive(Clone, Debug)]
pub struct SendConfigDto {
pub upstreams: HashMap<String, UpstreamConfigDto>,
pub client_headers: Headers,
pub server_headers: Headers,
pub consul: Option<Consul>,
pub kubernetes: Option<Kubernetes>,
pub typecfg: String,
pub extraparams: Extraparams,
}
pub fn serialize_config(data: &UpstreamsDashMap) -> String {
let config = GLOBAL_PARAMS.read().die("Failed to read config");
let mut snapshot: HashMap<String, UpstreamConfigDto> = HashMap::new();
for outer_entry in data.iter() {
let domain = outer_entry.key().to_string();
let mut paths: HashMap<String, PathConfigDto> = HashMap::new();
for inner_entry in outer_entry.value().iter() {
let path = inner_entry.key().to_string();
let (vec_inner, _atomic_val) = inner_entry.value();
if let Some(first) = vec_inner.first() {
let servers = vec_inner.iter().map(|item| format!("{}:{}", item.address, item.port)).collect();
let mut auth = None;
if let Some(ath) = first.authorization.clone() {
let mut a = HashMap::new();
a.insert("type".to_string(), ath.auth_type.clone().to_string());
a.insert("data".to_string(), ath.auth_cred.clone().to_string());
auth = Some(a);
}
paths.insert(
path,
PathConfigDto {
is_http2: first.is_http2,
healthcheck: first.healthcheck,
is_ssl: first.is_ssl,
to_https: first.to_https,
rate_limit: first.rate_limit,
x4xx_limit: first.x4xx_limit,
redirect_to: first.redirect_to.clone().as_ref().map(|s| s.to_string()),
authorization: auth,
servers,
},
);
}
}
snapshot.insert(domain, UpstreamConfigDto { paths });
}
let payload = SendConfigDto {
upstreams: snapshot,
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
to_yaml(&payload)
}
#[derive(Serialize, Debug)]
struct YamlAuth {
#[serde(rename = "type")]
auth_type: String,
data: String,
}
#[derive(Serialize)]
struct YamlPath {
servers: Vec<String>,
#[serde(skip_serializing_if = "is_false")]
is_http2: bool,
#[serde(skip_serializing_if = "is_false")]
to_https: bool,
#[serde(skip_serializing_if = "is_false")]
is_ssl: bool,
#[serde(skip_serializing_if = "is_true")] // healthcheck true is default
healthcheck: bool,
#[serde(skip_serializing_if = "is_zero_isize")]
rate_limit: isize,
#[serde(skip_serializing_if = "is_zero_u32")]
x4xx_limit: u32,
#[serde(skip_serializing_if = "Option::is_none")]
redirect_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
authorization: Option<YamlAuth>,
#[serde(skip_serializing_if = "Option::is_none")]
client_headers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
server_headers: Option<Vec<String>>,
}
#[derive(Serialize)]
struct YamlUpstream {
paths: HashMap<String, YamlPath>,
}
#[derive(Serialize)]
struct YamlConfig {
provider: String,
sticky_sessions: u64,
to_https: bool,
rate_limit: isize,
x4xx_limit: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
server_headers: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
client_headers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
authorization: Option<YamlAuth>,
upstreams: HashMap<String, YamlUpstream>,
}
fn is_false(v: &bool) -> bool {
!v
}
fn is_true(v: &bool) -> bool {
*v
}
fn is_zero_isize(v: &isize) -> bool {
*v == 0
}
fn is_zero_u32(v: &u32) -> bool {
*v == 0
}
pub fn to_yaml(config: &SendConfigDto) -> String {
let server_headers: Vec<String> = config
.server_headers
.get("GLOBAL_SERVER_HEADERS")
.map(|paths| {
paths
.value()
.iter()
.flat_map(|inner| inner.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect::<Vec<_>>())
.collect()
})
.unwrap_or_default();
let client_headers: Vec<String> = config
.client_headers
.get("GLOBAL_CLIENT_HEADERS")
.map(|paths| {
paths
.value()
.iter()
.flat_map(|inner| inner.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect::<Vec<_>>())
.collect()
})
.unwrap_or_default();
let authorization = config.extraparams.authentication.as_ref().map(|auth| YamlAuth {
auth_type: auth.auth_type.to_string(),
data: auth.auth_cred.to_string(),
});
let upstreams: HashMap<String, YamlUpstream> = config
.upstreams
.iter()
.map(|(domain, upstream)| {
let paths = upstream
.paths
.iter()
.map(|(path, pcfg)| {
let ch: Option<Vec<String>> = {
if let Some(domain_ref) = config.client_headers.get(domain.as_str()) {
if let Some(path_ref) = domain_ref.get(path.as_str()) {
let collected: Vec<String> = path_ref.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect();
if !collected.is_empty() { Some(collected) } else { None }
} else {
None
}
} else {
None
}
};
let sh: Option<Vec<String>> = {
if let Some(domain_ref) = config.server_headers.get(domain.as_str()) {
if let Some(path_ref) = domain_ref.get(path.as_str()) {
let collected: Vec<String> = path_ref.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect();
if !collected.is_empty() { Some(collected) } else { None }
} else {
None
}
} else {
None
}
};
let auth = pcfg.authorization.as_ref().map(|a| YamlAuth {
auth_type: a.get("type").cloned().unwrap_or_default(),
data: a.get("data").cloned().unwrap_or_default(),
});
(
path.clone(),
YamlPath {
servers: pcfg.servers.clone(),
is_http2: pcfg.is_http2,
is_ssl: pcfg.is_ssl,
healthcheck: pcfg.healthcheck,
rate_limit: pcfg.rate_limit,
x4xx_limit: pcfg.x4xx_limit,
to_https: pcfg.to_https,
redirect_to: pcfg.redirect_to.clone(),
authorization: auth,
client_headers: ch,
server_headers: sh,
},
)
})
.collect();
(domain.clone(), YamlUpstream { paths })
})
.collect();
let yaml_config = YamlConfig {
provider: config.typecfg.clone(),
sticky_sessions: config.extraparams.sticky_sessions,
to_https: config.extraparams.to_https,
rate_limit: config.extraparams.rate_limit,
x4xx_limit: config.extraparams.x4xx_limit,
server_headers,
client_headers,
authorization,
upstreams,
};
to_string(&yaml_config).unwrap_or_default()
}
pub async fn send_to_worker(data: String) {
let masterkey = MASTER_KEY.get().die("Master key not set");
if let Some(workers) = WORKERS.get() {
for worker in workers {
let prefix = if worker.tls { "https://" } else { "http://" };
let worker_url = format!("{}{}:{}", prefix, worker.address, worker.port);
let status = httpclient(HttpClientRequest {
method: "POST",
tls: worker.tls,
host: worker.address.as_ref(),
path: "/workerconf",
address: worker.address.as_ref(),
port: worker.port,
url: worker_url.as_str(),
payload: Bytes::from(data.clone()),
headers: Some(vec![("API-KEY", masterkey.clone().die("Master key must be set in the cluster mode").deref())]),
})
.await;
if (200..500).contains(&status.2) {
info!("Successfully updated Worker {}, status {}", worker_url, status.2);
} else {
error!("Cannot connect to worker node, status {}", status.2);
}
}
} else {
warn!("Workers list has not been initialized yet.");
}
}
-52
View File
@@ -1,52 +0,0 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use log::error;
use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep;
use std::path::Path;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::task;
pub async fn start(fp: String, toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-)
let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().unwrap();
let (local_tx, mut local_rx) = tokio::sync::mpsc::channel::<notify::Result<Event>>(1);
let _watcher_handle = task::spawn_blocking({
let parent_dir = parent_dir.to_path_buf(); // Move directory path into the closure
move || {
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = local_tx.blocking_send(res);
},
Config::default(),
)
.unwrap();
watcher.watch(&parent_dir, RecursiveMode::Recursive).unwrap();
let (_rtx, mut rrx) = tokio::sync::mpsc::channel::<bool>(1);
let _ = rrx.blocking_recv();
}
});
let mut start = Instant::now();
while let Some(event) = local_rx.recv().await {
match event {
Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if start.elapsed() > Duration::from_secs(2) {
start = Instant::now();
let snd = load_configuration(file_path, "filepath").await.0;
if let Some(snd) = snd {
toreturn.send(snd).await.unwrap();
}
}
}
_ => (),
},
Err(e) => error!("Watch error: {:?}", e),
}
}
}
+8 -7
View File
@@ -8,12 +8,13 @@ pub static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0); pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[allow(dead_code)] #[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator { unsafe impl GlobalAlloc for CountingAllocator {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 { unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed); ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed); ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout) System.alloc(layout)
} }
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed); DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout) System.dealloc(ptr, layout)
@@ -23,9 +24,9 @@ unsafe impl GlobalAlloc for CountingAllocator {
// Uncomment following lines and comment allocator in main.rs // Uncomment following lines and comment allocator in main.rs
// #[global_allocator] // #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator; // pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)] // #[allow(dead_code)]
fn for_example() { // fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed); // let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed); // let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before); // println!("Allocations : {}", after - before);
} // }
+17 -10
View File
@@ -1,23 +1,30 @@
use bytes::Bytes; use bytes::Bytes;
use http_body_util::Full; use http_body_util::Full;
use hyper::Request; use hyper::Request;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client; use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::rt::TokioExecutor; use hyper_util::rt::TokioExecutor;
use log::error;
pub async fn ping_grpc(addr: &str) -> bool { pub async fn ping_grpc(addr: &str) -> bool {
let connector = HttpConnector::new(); let connector = HttpConnector::new();
let client = Client::builder(TokioExecutor::new()).http2_only(true).build(connector); let client = Client::builder(TokioExecutor::new()).http2_only(true).build(connector);
let uri = format!("{}{}", addr, "/aralez.Probe/Ping"); let uri = format!("{}{}", addr, "/aralez.Probe/Ping");
let body = Full::new(Bytes::from(vec![0, 0, 0, 0, 0])); let body = Full::new(Bytes::from(vec![0, 0, 0, 0, 0]));
let request = Request::post(uri).header("content-type", "application/grpc").header("te", "trailers").body(body).unwrap();
match client.request(request).await { match Request::post(uri.clone()).header("content-type", "application/grpc").header("te", "trailers").body(body) {
Ok(resp) => resp Ok(request) => match client.request(request).await {
.headers() Ok(resp) => resp
.get("content-type") .headers()
.and_then(|v| v.to_str().ok()) .get("content-type")
.map(|v| v.starts_with("application/grpc")) .and_then(|v| v.to_str().ok())
.unwrap_or(false), .map(|v| v.starts_with("application/grpc"))
Err(_) => false, .unwrap_or(false),
Err(_) => false,
},
Err(e) => {
error!("Failed to create request: {}", e);
false
}
} }
} }
+119
View File
@@ -0,0 +1,119 @@
use crate::utils::grpc::ping_grpc;
use bytes::Bytes;
use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use std::sync::LazyLock;
use std::time::Duration;
pub static HC_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub struct HttpClientRequest<'a> {
pub method: &'a str,
pub tls: bool,
pub host: &'a str,
pub path: &'a str,
pub address: &'a str,
pub port: u16,
pub url: &'a str,
pub payload: Bytes,
pub headers: Option<Vec<(&'a str, &'a str)>>, // ← add this
}
pub async fn httpclient(req: HttpClientRequest<'_>) -> (bool, bool, u16) {
let HttpClientRequest {
method,
tls,
host,
path,
address,
port,
url,
payload,
headers,
} = req;
let method = match method {
"HEAD" => "HEAD",
"GET" => "GET",
"POST" => "POST",
_ => "GET",
};
let mut peer = HttpPeer::new((address, port), tls, host.to_string());
if tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
let mut is_h2 = false;
peer.options.total_connection_timeout = Option::from(Duration::from_secs(5));
peer.options.alpn = ALPN::H2H1;
let (mut http_session, _) = match HC_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
if let Some(msg) = e.context {
log::warn!("{}, type: {}", msg.as_str(), e.etype.as_str());
} else {
log::warn!("Fail to connect to addr: {}, tls {}, host {}", address, tls, host);
}
return (false, false, 500);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url).await {
return (true, true, 204);
}
}
}
let mut req = match RequestHeader::build(method, path.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("Failed to build request: {}", e);
return (false, false, 500);
}
};
req.insert_header("Host", host).ok();
let body_bytes = if method == "POST" {
req.insert_header("Content-Type", "application/json").ok();
req.insert_header("Content-Length", payload.len().to_string()).ok();
if let Some(headers) = headers {
for (k, v) in headers {
req.insert_header(k.to_owned(), v).ok();
}
}
payload
} else {
Bytes::new()
};
if let Err(e) = http_session.write_request_header(Box::new(req)).await {
log::warn!("Write header failed: {}", e);
return (false, false, 500);
}
if let Err(e) = http_session.write_request_body(body_bytes, true).await {
log::warn!("Write body failed: {}", e);
return (false, false, 500);
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
if !is_h2 && ping_grpc(url).await {
return (true, true, 204);
}
log::warn!("Health Check read failed ({}) : {} - {}", if is_h2 { "H2" } else { "H1" }, host, e);
return (false, false, 500);
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2, status)
}
+58 -74
View File
@@ -1,41 +1,54 @@
use crate::utils::grpc::ping_grpc; use crate::utils::deser::{send_to_worker, serialize_config};
use crate::utils::lazylock::REVERSE_STORE; use crate::utils::hcclient::{HttpClientRequest, httpclient};
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::lazylock::{IM_A_MASTER, NODE_STATUS, REVERSE_STORE};
use crate::utils::tools::*; use crate::utils::tools::*;
use crate::utils::types::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use bytes::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, warn}; use log::info;
use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::time::Duration; use std::time::Duration;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::time::interval; use tokio::time::{interval, timeout};
use tokio_native_tls::native_tls::TlsConnector; use tokio_native_tls::native_tls::TlsConnector;
const EMPTY_PAYLOAD: Bytes = Bytes::from_static(b"");
pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) { pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) {
let mut period = interval(Duration::from_secs(params.1)); let mut period = interval(Duration::from_secs(params.1));
let client = Client::builder().timeout(Duration::from_secs(params.1)).danger_accept_invalid_certs(true).build().unwrap(); if let Some(member) = NODE_STATUS.get() {
if let Some(mbr) = member.as_ref() {
match mbr.as_ref() {
"worker" => {
info!("Disabling health-checks. Running in cluster mode as {}", mbr);
return;
}
"master" => {
info!("Disabling client/server routing. Running in cluster mode as {}", mbr);
}
_ => {}
}
}
}
loop { loop {
tokio::select! { tokio::select! {
_ = period.tick() => { _ = period.tick() => {
// populate_upstreams(&upslist, &fullist, &idlist, params, &client).await; let totest = initiate_upstreams(&fullist, params.0).await;
let totest = build_upstreams(&fullist, params.0, &client).await;
if !compare_dashmaps(&totest, &upslist) { if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist); clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist); clone_idmap_into(&totest, &idlist);
if let Some(true) = IM_A_MASTER.get(){
let binary_output = serialize_config(&totest);
let _ = send_to_worker(binary_output).await;
}
REVERSE_STORE.clear(); REVERSE_STORE.clear();
} }
} }
} }
} }
} }
pub(crate) async fn initiate_upstreams(fullist: &UpstreamsDashMap, method: &str) -> UpstreamsDashMap {
pub async fn initiate_upstreams(fullist: UpstreamsDashMap) -> UpstreamsDashMap {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap();
build_upstreams(&fullist, "HEAD", &client).await
}
async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Client) -> UpstreamsDashMap {
let totest: UpstreamsDashMap = DashMap::new(); let totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist); let fclone = clone_dashmap(fullist);
for val in fclone.iter() { for val in fclone.iter() {
@@ -47,16 +60,16 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
let mut innervec = Vec::new(); let mut innervec = Vec::new();
for upstream in path_entry.value().0.iter() { for upstream in path_entry.value().0.iter() {
let tls = if upstream.healthcheck.unwrap_or(true) { let tls = if upstream.healthcheck {
detect_tls(upstream.address.as_ref(), &upstream.port, client).await detect_tls(upstream.address.as_ref(), &upstream.port, host).await
} else { } else {
(false, None) upstream.is_ssl
}; };
let mut scheme = InnerMap { let mut scheme = InnerMap {
address: upstream.address.clone(), address: upstream.address.clone(),
port: upstream.port, port: upstream.port,
is_ssl: tls.0, is_ssl: tls,
is_http2: matches!(tls.1, Some(Version::HTTP_2)), is_http2: upstream.is_http2,
to_https: upstream.to_https, to_https: upstream.to_https,
rate_limit: upstream.rate_limit, rate_limit: upstream.rate_limit,
x4xx_limit: upstream.x4xx_limit, x4xx_limit: upstream.x4xx_limit,
@@ -65,22 +78,29 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
authorization: upstream.authorization.clone(), authorization: upstream.authorization.clone(),
}; };
if scheme.healthcheck.unwrap_or(true) { if scheme.healthcheck {
let link = if tls.0 { let link = if tls {
format!("https://{}:{}{}", upstream.address, upstream.port, path) format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else { } else {
format!("http://{}:{}{}", upstream.address, upstream.port, path) format!("http://{}:{}{}", upstream.address, upstream.port, path)
}; };
let resp = httpclient(HttpClientRequest {
let resp = http_request(&link, method, "", client).await; method,
tls,
host,
path,
address: upstream.address.as_ref(),
port: upstream.port,
url: link.as_str(),
payload: EMPTY_PAYLOAD,
headers: None,
})
.await;
if resp.0 { if resp.0 {
if resp.1 { if resp.1 {
scheme.is_http2 = resp.1; scheme.is_http2 = resp.1;
} }
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
} }
} else { } else {
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
@@ -93,56 +113,20 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
totest totest
} }
async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -> (bool, bool) { async fn detect_tls(ip: &str, port: &u16, sni: &str) -> bool {
if !["POST", "GET", "HEAD"].contains(&method) { let addr = format!("{ip}:{port}");
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method); let timeout_duration = Duration::from_secs(2); // THINK !!!
return (false, false); let stream = match timeout(timeout_duration, TcpStream::connect(&addr)).await {
} Ok(Ok(s)) => s,
async fn send_request(client: &Client, method: &str, url: &str, payload: &str) -> Option<reqwest::Response> { Ok(Err(_)) => return false,
match method { Err(_) => return false,
"POST" => client.post(url).body(payload.to_owned()).send().await.ok(),
"GET" => client.get(url).send().await.ok(),
"HEAD" => client.head(url).send().await.ok(),
_ => None,
}
}
match send_request(client, method, url, payload).await {
Some(response) => {
let status = response.status().as_u16();
((99..499).contains(&status), false)
}
None => (ping_grpc(url).await, true),
}
}
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let addr = format!("{}:{}", ip, port);
let Ok(stream) = TcpStream::connect(&addr).await else {
return (false, Some(Version::HTTP_11));
}; };
let connector = tokio_native_tls::TlsConnector::from( let connector = tokio_native_tls::TlsConnector::from(
TlsConnector::builder() TlsConnector::builder()
.danger_accept_invalid_certs(true) .danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true) .danger_accept_invalid_hostnames(true)
.build() .build()
.unwrap(), .die("Failed to create TLS connector"),
); );
connector.connect(sni, stream).await.is_ok()
let tls = connector.connect(ip, stream).await.is_ok();
if tls {
let vers = detect_version(ip, *port, true, client).await;
return (tls, vers);
}
(tls, Some(Version::HTTP_11))
}
async fn detect_version(ip: &str, port: u16, is_tls: bool, client: &Client) -> Option<Version> {
let scheme = if is_tls { "https" } else { "http" };
let url = format!("{}://{}:{}", scheme, ip, port);
match client.get(&url).send().await.ok().map(|r| r.version()) {
Some(version) => Some(version),
None => Some(Version::HTTP_11),
}
} }
+223 -68
View File
@@ -1,85 +1,240 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints}; use crate::ingress::consul::ConsulService;
use crate::utils::structs::{GlobalServiceMapping, InnerMap}; use crate::ingress::kuberconsul::match_path;
use axum::http::{HeaderMap, HeaderValue}; use crate::ingress::kubernetes::KubeEndpointSliceList;
use crate::utils::types::{GlobalServiceMapping, InnerMap};
use ahash::HashMap;
use dashmap::DashMap; use dashmap::DashMap;
use reqwest::Client; use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use serde::{Deserialize, Serialize};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub static CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new(); #[derive(Debug, Default, Serialize, Deserialize, Clone)]
if let Some(token) = token { pub struct ConsulServicesInternal {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap()); #[serde(rename = "aralez.host")]
} pub host: String,
let to = Duration::from_secs(1); #[serde(rename = "aralez.path")]
let resp = client.get(url).timeout(to).send().await.ok()?; pub path: String,
if !resp.status().is_success() { #[serde(rename = "aralez.auth")]
eprintln!("Consul API returned status: {}", resp.status()); pub auth: Option<String>,
#[serde(rename = "aralez.redirect")]
pub redirect: Option<String>,
#[serde(rename = "aralez.rate")]
pub rate: Option<isize>,
#[serde(rename = "aralez.4xx_rate")]
pub xrate: Option<u32>,
#[serde(rename = "aralez.client_headers")]
pub client_headers: Option<Vec<String>>,
#[serde(rename = "aralez.server_headers")]
pub server_headers: Option<Vec<String>>,
#[serde(rename = "aralez.to_https")]
pub to_https: Option<bool>,
}
pub type ConsulServices = HashMap<String, ConsulServicesInternal>;
pub async fn for_consul_list(url: &str, token: Option<String>) -> Option<ConsulServices> {
if let Some(data) = getfromapi(url, token, "consul").await {
let yo = parse_services(data);
if let Ok(y) = yo {
return Some(y);
}
return None; return None;
} }
let mut inner_vec = Vec::new(); None
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); }
let endpoints: Vec<ConsulService> = resp.json().await.ok()?;
for subsets in endpoints { fn parse_services(json: Vec<u8>) -> Result<ConsulServices, serde_json::Error> {
let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone(); let raw: HashMap<String, Vec<String>> = serde_json::from_slice(&json)?;
let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port; Ok(raw
let to_add = Arc::from(InnerMap { .into_iter()
address: Arc::from(&*addr), .map(|(service, tags)| {
port: prt, let mut internal = ConsulServicesInternal::default();
is_ssl: false, for tag in tags {
is_http2: false, if let Some((key, value)) = tag.split_once('=') {
to_https: conf.to_https.unwrap_or(false), match key {
rate_limit: conf.rate_limit, "aralez.host" => internal.host = value.to_string(),
x4xx_limit: conf.x4xx_limit, "aralez.path" => internal.path = value.to_string(),
redirect_to: None, "aralez.rate" => internal.rate = value.parse::<isize>().ok(),
healthcheck: None, "aralez.4xx_rate" => internal.xrate = value.parse::<u32>().ok(),
authorization: None, "aralez.to_https" => internal.to_https = value.parse::<bool>().ok(),
}); "aralez.auth" => internal.auth = Option::from(value.to_string()),
inner_vec.push(to_add); "aralez.redirect" => internal.redirect = Option::from(value.to_string()),
} "aralez.client_header" => internal.client_headers.get_or_insert_default().push(value.to_string()),
match_path(conf, &upstreams, inner_vec); "aralez.server_header" => internal.server_headers.get_or_insert_default().push(value.to_string()),
Some(upstreams) _ => {}
}
}
}
(service, internal)
})
.collect())
}
pub async fn for_consul(url: &str, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, token, "consul").await {
let endpoints: Vec<ConsulService> = serde_json::from_slice(&data).ok()?;
let mut inner_vec = Vec::new();
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
for subsets in endpoints {
let to_add = Arc::from(InnerMap {
address: Arc::from(&*subsets.address),
port: subsets.port,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit.unwrap_or(0),
x4xx_limit: conf.x4xx_limit.unwrap_or(0),
redirect_to: conf.redirect_to.clone().map(Arc::<str>::from),
healthcheck: true,
authorization: None,
});
inner_vec.push(to_add);
}
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
};
None
} }
pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let to = Duration::from_secs(10); if let Some(data) = getfromapi(url, Some(token.to_string()), "kubernetes").await {
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?; let slice_list: KubeEndpointSliceList = serde_json::from_slice(&data).ok()?;
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?; let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
if !resp.status().is_success() { let mut inner_vec = Vec::new();
eprintln!("Kubernetes API returned status: {}", resp.status());
return None;
}
let endpoints: KubeEndpoints = resp.json().await.ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); for slice in slice_list.items {
let ports = match &slice.ports {
Some(p) if !p.is_empty() => p,
_ => continue,
};
if let Some(subsets) = endpoints.subsets { for ep in &slice.endpoints {
for subset in subsets { let is_ready = ep.conditions.as_ref().and_then(|c| c.ready).unwrap_or(true);
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
let mut inner_vec = Vec::new(); if !is_ready {
for addr in addresses { continue;
for port in &ports { }
// let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
let to_add = Arc::from(InnerMap { for addr in &ep.addresses {
address: Arc::from(addr.ip.clone()), for port in ports {
port: port.port, if let Some(port_num) = port.port {
is_ssl: false, let to_add = Arc::from(InnerMap {
is_http2: false, address: Arc::from(addr.as_str()),
to_https: conf.to_https.unwrap_or(false), port: port_num,
rate_limit: conf.rate_limit, is_ssl: false,
x4xx_limit: conf.x4xx_limit, is_http2: false,
healthcheck: None, to_https: conf.to_https.unwrap_or(false),
redirect_to: None, rate_limit: conf.rate_limit.unwrap_or(0),
authorization: None, x4xx_limit: conf.x4xx_limit.unwrap_or(0),
}); healthcheck: true,
inner_vec.push(to_add); redirect_to: None,
authorization: None,
});
inner_vec.push(to_add);
}
} }
} }
match_path(conf, &upstreams, inner_vec.clone());
} }
} }
if !inner_vec.is_empty() {
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
}
} }
Some(upstreams) None
}
pub async fn getfromapi(url: &str, token: Option<String>, provider: &str) -> Option<Vec<u8>> {
let (host, port, path, is_tls) = parse_url(url).ok()?;
let mut peer = HttpPeer::new((host, port), is_tls, host.to_string());
peer.options.total_connection_timeout = Some(Duration::from_secs(5));
peer.options.read_timeout = Some(Duration::from_secs(5));
if is_tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
peer.options.alpn = ALPN::H2H1;
}
let mut http_session = CONNECTOR.get_http_session(&peer).await.ok()?;
let mut req = RequestHeader::build("GET", path.as_bytes(), None).ok()?;
let host_header = if (is_tls && port == 443) || (!is_tls && port == 80) {
host.to_string()
} else {
format!("{}:{}", host, port)
};
req.insert_header("Host", host_header).ok()?;
req.insert_header("Accept", "application/json").ok()?;
match provider {
"consul" => {
if let Some(token) = token {
req.insert_header("X-Consul-Token", token).ok()?;
}
}
"kubernetes" => {
if let Some(token) = token {
req.insert_header("Authorization", format!("Bearer {}", token)).ok()?;
}
}
_ => {}
}
if http_session.0.write_request_header(Box::new(req)).await.is_err() {
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
return None;
}
let status = match http_session.0.read_response_header().await {
Ok(_) => http_session.0.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("API call failed to read response header for {}: {}", url, e);
500
}
};
let mut body_bytes = Vec::new();
if status == 200 {
while let Ok(Some(chunk)) = http_session.0.read_response_body().await {
body_bytes.extend_from_slice(&chunk);
}
}
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
if status == 200 && !body_bytes.is_empty() { Some(body_bytes) } else { None }
}
fn parse_url(url: &str) -> Result<(&str, u16, &str, bool), &'static str> {
let is_https = url.starts_with("https://");
let default_port = if is_https { 443 } else { 80 };
let no_scheme = url.split_once("://").map_or(url, |(_, rest)| rest);
let (authority, uri) = no_scheme.find('/').map_or((no_scheme, "/"), |i| (&no_scheme[..i], &no_scheme[i..]));
let (host, port) = match authority.split_once(':') {
Some((h, p)) => {
let port_num = p.parse::<u16>().map_err(|_| "Invalid port number")?;
(h, port_num)
}
None => (authority, default_port),
};
if host.is_empty() {
return Err("Empty host in URL");
}
Ok((host, port, uri, is_https))
} }
-228
View File
@@ -1,228 +0,0 @@
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
pub struct KubeEndpoints {
pub subsets: Option<Vec<KubeSubset>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeSubset {
pub addresses: Option<Vec<KubeAddress>>,
pub ports: Option<Vec<KubePort>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeAddress {
pub ip: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubePort {
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "ServiceTaggedAddresses")]
pub tagged_addresses: HashMap<String, ConsulTaggedAddress>,
}
#[derive(Debug, Deserialize)]
pub struct ConsulTaggedAddress {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "Port")]
pub port: u16,
}
#[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
if let Some(list) = lt {
match upstreams.get(&*i.hostname.clone()) {
Some(upstr) => {
for (k, v) in list {
upstr.value().insert(k.to_owned(), v);
}
}
None => {
upstreams.insert(Arc::from(i.hostname.clone()), list);
}
};
}
}
pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
match conf.path {
Some(ref p) => {
upstreams.insert(Arc::from(p.clone()), (values, AtomicUsize::new(0)));
}
None => {
upstreams.insert(Arc::from("/"), (values, AtomicUsize::new(0)));
}
}
}
async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();
contents.trim().to_string()
}
#[async_trait]
pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
}
pub struct KubernetesDiscovery;
pub struct ConsulDiscovery;
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).unwrap().to_string();
let path = kuber.tokenpath.unwrap_or("/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let namespace = get_current_namespace().unwrap_or_else(|| "default".to_string());
let token = read_token(path.as_str()).await;
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
if let Some(svc) = kuber.services {
for service in svc {
let header_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut hl = Vec::new();
build_headers(&service.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match service.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(Arc::from(path.as_str()), hl);
// header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl);
config.client_headers.insert(Arc::from(service.hostname.clone()), header_list);
}
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, service.hostname);
// let url = format!("https://{}/api/v1/namespaces/{}/endpoints?labelSelector=app", server, namespace);
let list = httpclient::for_kuber(&url, &token, &service).await;
// println!("{:?}", list);
list_to_upstreams(list, &upstreams, &service);
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
}
sleep(Duration::from_secs(5)).await;
}
}
}
}
fn get_current_namespace() -> Option<String> {
let ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
if Path::new(ns_path).exists() {
if let Ok(contents) = fs::read_to_string(ns_path) {
return Some(contents.trim().to_string());
}
}
std::env::var("POD_NAMESPACE").ok()
}
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let consul_data = servers.get(num).unwrap().to_string();
let ss = consul_data + "/v1/catalog/service/";
if let Some(svc) = consul.services {
for i in svc {
let header_list = DashMap::new();
let mut hl = Vec::new();
build_headers(&i.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match i.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.client_headers.insert(Arc::from(i.hostname.clone()), header_list);
}
let pref = ss.clone() + &i.upstream;
let list = httpclient::for_consul(pref, consul.token.clone(), &i).await;
list_to_upstreams(list, &upstreams, &i);
}
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
sleep(Duration::from_secs(5)).await;
}
}
}
async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(upstreams, prev_upstreams) {
let tosend: Configuration = Configuration {
upstreams: Default::default(),
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(upstreams, prev_upstreams);
clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend);
};
None
}
+11 -2
View File
@@ -1,12 +1,14 @@
use crate::utils::types::{Configuration, WorkerNodes};
use dashmap::DashMap; use dashmap::DashMap;
use moka::sync::Cache; use moka::sync::Cache;
use pingora_cache::MemCache;
use pingora_cache::eviction::simple_lru::Manager; use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::lock::CacheLock; use pingora_cache::lock::CacheLock;
use pingora_cache::MemCache;
use pingora_limits::rate::Rate; use pingora_limits::rate::Rate;
use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::OnceLock;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::sync::{OnceLock, RwLock};
use std::time::Duration; use std::time::Duration;
pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new); pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
@@ -17,3 +19,10 @@ pub static MEM_CACHE: LazyLock<MemCache> = LazyLock::new(MemCache::new);
pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30))); pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30)));
pub static EVICTION: OnceLock<Manager> = OnceLock::new(); pub static EVICTION: OnceLock<Manager> = OnceLock::new();
pub static CACHE_TTL: OnceLock<u64> = OnceLock::new(); pub static CACHE_TTL: OnceLock<u64> = OnceLock::new();
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub static NODE_STATUS: OnceLock<Option<Arc<str>>> = OnceLock::new();
pub static MASTER_KEY: OnceLock<Option<Arc<str>>> = OnceLock::new();
pub static GLOBAL_PARAMS: LazyLock<RwLock<Configuration>> = LazyLock::new(|| RwLock::new(Configuration::default()));
pub static IM_A_MASTER: OnceLock<bool> = OnceLock::new();
pub static WORKERS: OnceLock<Vec<WorkerNodes>> = OnceLock::new();
+107 -34
View File
@@ -1,9 +1,10 @@
use crate::utils::lazylock::EVICTION; use crate::utils::lazylock::EVICTION;
use crate::utils::tools::Die;
use pingora_cache::eviction::EvictionManager; use pingora_cache::eviction::EvictionManager;
use pingora_http::Method; use pingora_http::Method;
use pingora_http::StatusCode; use pingora_http::StatusCode;
use pingora_http::Version; use pingora_http::Version;
use prometheus::{register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge, Histogram, IntCounter, IntCounterVec, IntGauge}; use prometheus::{Histogram, IntCounter, IntCounterVec, IntGauge, register_histogram, register_int_counter, register_int_counter_vec, register_int_gauge};
use std::sync::Arc; use std::sync::Arc;
use std::sync::LazyLock; use std::sync::LazyLock;
use std::time::Duration; use std::time::Duration;
@@ -16,19 +17,28 @@ pub struct MetricTypes {
pub version: Version, pub version: Version,
} }
pub static OPEN_FILES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").unwrap()); pub static OPEN_FILES: LazyLock<IntGauge> =
pub static LOGGING_ERRORS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_logging_errors", "Number of log errors").unwrap()); LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").die("Failed to register OPEN_FILES metric"));
pub static MEMORY_USAGE: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").unwrap()); pub static LOGGING_ERRORS: LazyLock<IntGauge> =
pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap()); LazyLock::new(|| register_int_gauge!("aralez_logging_errors", "Number of log errors").die("Failed to register LOGGING_ERRORS metric"));
pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap()); pub static MEMORY_USAGE: LazyLock<IntGauge> =
LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").die("Failed to register MEMORY_USAGE metric"));
pub static ACTIVE_SESSIONS: LazyLock<IntGauge> =
LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").die("Failed to register ACTIVE_SESSIONS metric"));
pub static REQUEST_COUNT: LazyLock<IntCounter> =
LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").die("Failed to register REQUEST_COUNT metric"));
pub static CACHE_SIZE_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_size_bytes", "Current cache size in bytes").unwrap()); pub static CACHE_SIZE_BYTES: LazyLock<IntGauge> =
pub static CACHE_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_items", "Current number of cached objects").unwrap()); LazyLock::new(|| register_int_gauge!("aralez_cache_size_bytes", "Current cache size in bytes").die("Failed to register CACHE_SIZE_BYTES metric"));
pub static CACHE_EVICTED_BYTES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_bytes_total", "Total bytes evicted from cache").unwrap()); pub static CACHE_ITEMS: LazyLock<IntGauge> =
LazyLock::new(|| register_int_gauge!("aralez_cache_items", "Current number of cached objects").die("Failed to register CACHE_ITEMS metric"));
pub static CACHE_EVICTED_BYTES: LazyLock<IntGauge> =
LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_bytes_total", "Total bytes evicted from cache").die("Failed to register CACHE_EVICTED_BYTES metric"));
pub static CACHE_EVICTED_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_items_total", "Total cache items evicted").unwrap()); pub static CACHE_EVICTED_ITEMS: LazyLock<IntGauge> =
LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_items_total", "Total cache items evicted").die("Failed to register CACHE_EVICTED_ITEMS metric"));
pub static RESPONSE_CODES: LazyLock<IntCounterVec> = pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap()); LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).die("Failed to register RESPONSE_CODES metric"));
pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| { pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!( register_histogram!(
@@ -36,39 +46,90 @@ pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
"Response latency in seconds", "Response latency in seconds",
vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0] vec![0.001, 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
) )
.unwrap() .die("Failed to register RESPONSE_LATENCY metric")
}); });
pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> = pub static REQUESTS_BY_METHOD: LazyLock<IntCounterVec> = LazyLock::new(|| {
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).unwrap()); register_int_counter_vec!("aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"]).die("Failed to register REQUESTS_BY_METHOD metric")
});
pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> = pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> = LazyLock::new(|| {
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).unwrap()); register_int_counter_vec!("aralez_requests_by_upstream", "Number of requests by UPSTREAM server", &["upstream"]).die("Failed to register REQUESTS_BY_UPSTREAM metric")
});
pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> = pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> = LazyLock::new(|| {
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap()); register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).die("Failed to register REQUESTS_BY_VERSION metric")
});
pub static HTTP_11_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.1"]));
pub static HTTP_20_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/2.0"]));
pub static HTTP_30_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/3.0"]));
pub static HTTP_10_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["HTTP/1.0"]));
pub static HTTP_UNK_COUNT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_VERSION.with_label_values(&["Unknown"]));
pub static METHOD_GET: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["GET"]));
pub static METHOD_POST: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["POST"]));
pub static METHOD_PUT: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PUT"]));
pub static METHOD_DELETE: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["DELETE"]));
pub static METHOD_HEAD: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["HEAD"]));
pub static METHOD_OPTIONS: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OPTIONS"]));
pub static METHOD_PATCH: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["PATCH"]));
pub static METHOD_OTHER: LazyLock<IntCounter> = LazyLock::new(|| REQUESTS_BY_METHOD.with_label_values(&["OTHER"]));
pub static STATUS_100: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["100"]));
pub static STATUS_101: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["101"]));
pub static STATUS_200: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["200"]));
pub static STATUS_204: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["204"]));
pub static STATUS_301: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["301"]));
pub static STATUS_302: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["302"]));
pub static STATUS_304: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["304"]));
pub static STATUS_400: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["400"]));
pub static STATUS_403: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["403"]));
pub static STATUS_404: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["404"]));
pub static STATUS_500: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["500"]));
pub static STATUS_501: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["501"]));
pub static STATUS_502: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["502"]));
pub static STATUS_503: LazyLock<IntCounter> = LazyLock::new(|| RESPONSE_CODES.with_label_values(&["503"]));
pub fn calc_metrics(metric_types: &MetricTypes) { pub fn calc_metrics(metric_types: &MetricTypes) {
REQUEST_COUNT.inc(); REQUEST_COUNT.inc();
let version_str = match metric_types.version { match metric_types.version {
Version::HTTP_11 => "HTTP/1.1", Version::HTTP_11 => HTTP_11_COUNT.inc(),
Version::HTTP_2 => "HTTP/2.0", Version::HTTP_2 => HTTP_20_COUNT.inc(),
Version::HTTP_3 => "HTTP/3.0", Version::HTTP_3 => HTTP_30_COUNT.inc(),
Version::HTTP_10 => "HTTP/1.0", Version::HTTP_10 => HTTP_10_COUNT.inc(),
_ => "Unknown", _ => HTTP_UNK_COUNT.inc(),
}; };
REQUESTS_BY_VERSION.with_label_values(&[version_str]).inc(); match metric_types.method.as_str() {
RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(StatusCode::GONE).as_str()]).inc(); "GET" => METHOD_GET.inc(),
REQUESTS_BY_METHOD.with_label_values(&[metric_types.method.as_str()]).inc(); "POST" => METHOD_POST.inc(),
"PUT" => METHOD_PUT.inc(),
"DELETE" => METHOD_DELETE.inc(),
"HEAD" => METHOD_HEAD.inc(),
"OPTIONS" => METHOD_OPTIONS.inc(),
"PATCH" => METHOD_PATCH.inc(),
_ => METHOD_OTHER.inc(),
}
let code = metric_types.code.unwrap_or(StatusCode::GONE);
match code.as_u16() {
100 => STATUS_100.inc(),
101 => STATUS_101.inc(),
200 => STATUS_200.inc(),
204 => STATUS_204.inc(),
301 => STATUS_301.inc(),
302 => STATUS_302.inc(),
304 => STATUS_304.inc(),
400 => STATUS_400.inc(),
403 => STATUS_403.inc(),
404 => STATUS_404.inc(),
500 => STATUS_500.inc(),
501 => STATUS_501.inc(),
502 => STATUS_502.inc(),
503 => STATUS_503.inc(),
_ => RESPONSE_CODES.with_label_values(&[code.as_str()]).inc(), // Fallback for rare status codes
}
REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc(); REQUESTS_BY_UPSTREAM.with_label_values(&[metric_types.upstream.as_ref()]).inc();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64()); RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
} }
pub(crate) fn get_memory_usage() -> usize { pub(crate) fn get_memory_usage() -> usize {
@@ -87,3 +148,15 @@ pub(crate) fn get_memory_usage() -> usize {
pub fn get_open_files() -> usize { pub fn get_open_files() -> usize {
std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0) std::fs::read_dir("/proc/self/fd").map(|dir| dir.count()).unwrap_or(0)
} }
pub async fn calc_cache_metrics() {
loop {
if let Some(eviction) = EVICTION.get() {
CACHE_SIZE_BYTES.set(eviction.total_size() as i64);
CACHE_ITEMS.set(eviction.total_items() as i64);
CACHE_EVICTED_BYTES.set(eviction.evicted_size() as i64);
CACHE_EVICTED_ITEMS.set(eviction.evicted_items() as i64);
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
}
+7 -6
View File
@@ -1,15 +1,16 @@
pub mod auth;
pub mod discovery;
mod filewatch;
pub mod fordebug; pub mod fordebug;
pub mod grpc; pub mod grpc;
pub mod hcclient;
pub mod healthcheck; pub mod healthcheck;
pub mod httpclient; pub mod httpclient;
pub mod jwt; // pub mod ingress;
pub mod kuberconsul; pub mod deser;
pub mod lazylock; pub mod lazylock;
pub mod metrics; pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state; pub mod state;
pub mod structs;
pub mod tools; pub mod tools;
pub mod types;
pub(crate) mod watch;
pub use watch::*;
+76 -26
View File
@@ -1,19 +1,17 @@
use crate::utils::deser::{send_to_worker, serialize_config};
use crate::utils::healthcheck; use crate::utils::healthcheck;
use crate::utils::lazylock::REVERSE_STORE; use crate::utils::lazylock::{DOMAINS, GLOBAL_PARAMS, IM_A_MASTER, MASTER_KEY, NODE_STATUS, REVERSE_STORE, WORKERS};
use crate::utils::state::{is_first_run, mark_not_first_run}; use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*; use crate::utils::tools::{Die, clone_dashmap_into};
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams}; use crate::utils::types::*;
use crate::web::logging::log_builder;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info, warn}; use log::{error, info, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock};
use std::{env, fs}; use std::{env, fs};
pub static DOMAINS: LazyLock<DashMap<String, bool>> = LazyLock::new(DashMap::new);
pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) { pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>, String) {
let mut conf_files = Vec::new(); let mut conf_files = Vec::new();
let yaml_data = match kind { let yaml_data = match kind {
@@ -50,8 +48,8 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
return (None, err_msg); return (None, err_msg);
} }
let mut confdir = Path::new(d).parent().unwrap().to_path_buf(); let mut confdir = Path::new(d).parent().die("Invalid configuration path").to_path_buf();
let mut autocfg = Path::new(d).parent().unwrap().to_path_buf(); let mut autocfg = Path::new(d).parent().die("Invalid configuration path").to_path_buf();
autocfg.push("autoconfigs"); autocfg.push("autoconfigs");
if fs::metadata(autocfg.clone()).is_err() { if fs::metadata(autocfg.clone()).is_err() {
@@ -91,7 +89,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
} }
info!("Reading upstreams from {}", d); info!("Reading upstreams from {}", d);
data // [2606:4700:2ff9::1]:443 data
} }
"content" => { "content" => {
info!("Reading upstreams from API post body"); info!("Reading upstreams from API post body");
@@ -173,10 +171,10 @@ async fn populate_headers_and_auth(config: &mut Configuration, parsed: &Config)
let server_global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new(); let server_global_headers: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
server_global_headers.insert(Arc::from("/"), sh); server_global_headers.insert(Arc::from("/"), sh);
config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers); config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers);
config.extraparams.to_https = parsed.to_https; config.extraparams.to_https = parsed.to_https.unwrap_or(false);
config.extraparams.sticky_sessions = parsed.sticky_sessions; config.extraparams.sticky_sessions = parsed.sticky_sessions.unwrap_or(0);
config.extraparams.rate_limit = parsed.rate_limit; config.extraparams.rate_limit = parsed.rate_limit.unwrap_or(0);
config.extraparams.x4xx_limit = parsed.x4xx_limit; config.extraparams.x4xx_limit = parsed.x4xx_limit.unwrap_or(0);
if let Some(rate) = &parsed.rate_limit { if let Some(rate) = &parsed.rate_limit {
info!("Applied Global Rate Limit : {} request per second", rate); info!("Applied Global Rate Limit : {} request per second", rate);
@@ -226,12 +224,12 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
server_list.push(Arc::from(InnerMap { server_list.push(Arc::from(InnerMap {
address: Arc::from(ip), address: Arc::from(ip),
port, port,
is_ssl: false, is_ssl: path_config.is_ssl.unwrap_or(false),
is_http2: false, is_http2: path_config.is_http2.unwrap_or(false),
to_https: path_config.to_https.unwrap_or(false), to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit, rate_limit: path_config.rate_limit.unwrap_or(0),
x4xx_limit: path_config.x4xx_limit, x4xx_limit: path_config.x4xx_limit.unwrap_or(0),
healthcheck: path_config.healthcheck, healthcheck: path_config.healthcheck.unwrap_or(true),
redirect_to: redirect_link, redirect_to: redirect_link,
authorization: path_auth, authorization: path_auth,
})); }));
@@ -242,30 +240,49 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
} }
config.client_headers.insert(Arc::from(hostname.clone()), client_header_list); config.client_headers.insert(Arc::from(hostname.clone()), client_header_list);
config.server_headers.insert(Arc::from(hostname.clone()), server_header_list); config.server_headers.insert(Arc::from(hostname.clone()), server_header_list);
imtdashmap.insert(Arc::from(hostname.clone()), path_map); imtdashmap.insert(Arc::from(hostname.clone()), path_map);
} }
{
let mut config_guard = GLOBAL_PARAMS.write().die("Global config lock poisoned");
config_guard.client_headers = config.client_headers.clone();
config_guard.server_headers = config.server_headers.clone();
config_guard.extraparams = config.extraparams.clone();
config_guard.kubernetes = config.kubernetes.clone();
config_guard.consul = config.consul.clone();
config_guard.typecfg = config.typecfg.clone();
}
if is_first_run() { if is_first_run() {
clone_dashmap_into(&imtdashmap, &config.upstreams); clone_dashmap_into(&imtdashmap, &config.upstreams);
mark_not_first_run(); mark_not_first_run();
} else { } else {
let y = clone_dashmap(&imtdashmap); let r = healthcheck::initiate_upstreams(&imtdashmap, "HEAD").await;
let r = healthcheck::initiate_upstreams(y).await;
clone_dashmap_into(&r, &config.upstreams); clone_dashmap_into(&r, &config.upstreams);
} }
info!("Upstream Config:"); info!("Upstream Config:");
REVERSE_STORE.clear(); REVERSE_STORE.clear();
print_upstreams(&config.upstreams, &config.extraparams);
if let Some(true) = IM_A_MASTER.get() {
if !is_first_run() {
let binary_output = serialize_config(&config.upstreams);
let _ = send_to_worker(binary_output).await;
}
}
// print_upstreams(&config.upstreams, &config.extraparams);
} }
} }
pub fn parce_main_config(path: &str) -> AppConfig { pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap(); let data = fs::read_to_string(path).die("Failed to read main config file");
let mut cfo: AppConfig = noyalib::from_str(&data).expect("Failed to parse main config file"); let mut cfo: AppConfig = noyalib::from_str(&data).die("Failed to parse main config file");
if let Ok(jwt_key) = env::var("JWT_KEY") { if let Ok(jwt_key) = env::var("MASTER_KEY") {
cfo.master_key = Some(jwt_key);
} else if let Ok(jwt_key) = env::var("JWT_KEY") {
cfo.master_key = Some(jwt_key); cfo.master_key = Some(jwt_key);
}; };
log_builder(&cfo, &cfo.log_file); // log_builder(&cfo, &cfo.log_file);
cfo.hc_method = cfo.hc_method.to_uppercase(); cfo.hc_method = cfo.hc_method.to_uppercase();
if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') { if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() { if let Ok(port) = port_str.parse::<u16>() {
@@ -283,6 +300,39 @@ pub fn parce_main_config(path: &str) -> AppConfig {
} }
cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone()); cfo.proxy_tls_grade = parce_tls_grades(cfo.proxy_tls_grade.clone());
if let Some(member) = cfo.cluster_member.clone() {
NODE_STATUS.set(Some(Arc::from(member.as_str()))).die("Config can only be initialized once");
let mk = cfo.master_key.clone().die("In CLuster mode master key must be set");
MASTER_KEY.set(Some(Arc::from(mk.as_str()))).die("Config can only be initialized once");
if member == "master" {
IM_A_MASTER.set(true).die("Master status can only be initialized once");
let worker_str = cfo.worker_nodes.as_deref().die("At least one worker must be set in master mode");
let mut wks = Vec::new();
for worker in worker_str.split(',') {
let clean = worker.replace("//", "");
let wk_vec = clean.trim().split(':').collect::<Vec<&str>>();
let wk = WorkerNodes {
address: Arc::from(wk_vec[1]),
port: wk_vec[2].parse::<u16>().die("Invalid worker port number"),
tls: match wk_vec[0] {
"https" => true,
"http" => false,
_ => panic!("invalid protocol string"),
},
key: cfo.master_key.clone().map(|s| Arc::from(s)),
};
wks.push(wk);
// println!("{:?}", wk);
// if let Some((host, port_str)) = worker.trim().split_once(':') {
// let port = port_str.parse::<u16>().die("Invalid worker port number");
// wks.push((host.to_string(), port));
// } else {
// panic!("Invalid worker format, expected host:port");
// }
}
WORKERS.set(wks).die("Workers list can only be initialized once");
}
}
cfo cfo
} }
+3 -13
View File
@@ -1,3 +1,4 @@
use crate::utils::tools::Die;
use std::sync::{LazyLock, RwLock}; use std::sync::{LazyLock, RwLock};
#[derive(Debug)] #[derive(Debug)]
@@ -8,22 +9,11 @@ pub struct SharedState {
pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true })); pub static GLOBAL_STATE: LazyLock<RwLock<SharedState>> = LazyLock::new(|| RwLock::new(SharedState { first_run: true }));
pub fn mark_not_first_run() { pub fn mark_not_first_run() {
let mut state = GLOBAL_STATE.write().unwrap(); let mut state = GLOBAL_STATE.write().die("Writing global state failed");
state.first_run = false; state.first_run = false;
} }
pub fn is_first_run() -> bool { pub fn is_first_run() -> bool {
let state = GLOBAL_STATE.read().unwrap(); let state = GLOBAL_STATE.read().die("Writing global state failed");
state.first_run state.first_run
} }
/*
impl SharedState {
pub fn mark_first_run(&mut self) {
self.first_run = false;
}
pub fn is_first_run(&self) -> bool {
self.first_run
}
}
*/
+91 -66
View File
@@ -1,34 +1,32 @@
use crate::tls::load; use crate::tls::load;
use crate::tls::load::CertificateConfig; use crate::tls::load::CertificateConfig;
use crate::utils::structs::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::types::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, info}; use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use privdrop::PrivDrop; use privdrop::PrivDrop;
use serde_json::{json, Value}; use serde_json::{Value, json};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::any::type_name;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Write; use std::fmt::{Debug, Write};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write as IoWrite; use std::io::Write as IoWrite;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::net::TcpListener; use std::net::TcpListener;
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::panic::Location;
use std::process::Command;
use std::str::FromStr; use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::{fs, process, thread, time}; use std::{env, fs, process, thread, time};
pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams) { pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams) {
let mut out = String::new(); let mut out = String::new();
for host_entry in upstreams.iter() { for host_entry in upstreams.iter() {
writeln!(out, "Hostname: {}", host_entry.key()).unwrap(); writeln!(out, "Hostname: {}", host_entry.key()).unwrap_or_default();
for path_entry in host_entry.value().iter() { for path_entry in host_entry.value().iter() {
writeln!(out, " Path: {}", path_entry.key()).unwrap(); writeln!(out, " Path: {}", path_entry.key()).unwrap_or_default();
for f in path_entry.value().0.clone() { for f in path_entry.value().0.clone() {
writeln!( writeln!(
out, out,
@@ -38,33 +36,15 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams)
f.is_ssl, f.is_ssl,
f.is_http2, f.is_http2,
f.to_https, f.to_https,
f.rate_limit.unwrap_or(extraparams.rate_limit.unwrap_or(0)), if f.rate_limit > 0 { f.rate_limit } else { extraparams.rate_limit },
f.x4xx_limit.unwrap_or(extraparams.x4xx_limit.unwrap_or(0)) if f.x4xx_limit > 0 { f.x4xx_limit } else { extraparams.x4xx_limit }
) )
.unwrap(); .unwrap_or_default();
} }
} }
} }
info!("\n{}", out.trim_end()); info!("\n{}", out.trim_end());
} }
#[allow(dead_code)]
pub fn typeoff<T>(_: T) {
let to = type_name::<T>();
println!("{:?}", to);
}
#[allow(dead_code)]
pub fn string_to_bool(val: Option<&str>) -> Option<bool> {
match val {
Some(v) => match v {
"yes" => Some(true),
"true" => Some(true),
_ => Some(false),
},
None => Some(false),
}
}
pub fn clone_dashmap(original: &UpstreamsDashMap) -> UpstreamsDashMap { pub fn clone_dashmap(original: &UpstreamsDashMap) -> UpstreamsDashMap {
let new_map: UpstreamsDashMap = DashMap::new(); let new_map: UpstreamsDashMap = DashMap::new();
@@ -159,9 +139,9 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
x.port, x.port,
x.is_http2, x.is_http2,
x.to_https, x.to_https,
x.x4xx_limit.unwrap_or_default(), x.x4xx_limit,
x.rate_limit.unwrap_or_default(), x.rate_limit,
x.healthcheck.unwrap_or_default(), x.healthcheck,
x.authorization x.authorization
) )
.unwrap_or(()); .unwrap_or(());
@@ -182,9 +162,9 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
pub fn listdir(dir: String) -> Vec<load::CertificateConfig> { pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
let mut f = HashMap::new(); let mut f = HashMap::new();
let mut certificate_configs: Vec<load::CertificateConfig> = vec![]; let mut certificate_configs: Vec<load::CertificateConfig> = vec![];
let paths = fs::read_dir(dir).unwrap(); let paths = fs::read_dir(dir).die("listing certificates directory failed");
for path in paths { for path in paths {
let path_str = path.unwrap().path().to_str().unwrap().to_owned(); let path_str = path.die("getting certs path").path().to_str().die("certs pat t &str").to_owned();
if path_str.ends_with(".crt") { if path_str.ends_with(".crt") {
let name = path_str.replace(".crt", ""); let name = path_str.replace(".crt", "");
let mut inner = vec![]; let mut inner = vec![];
@@ -209,33 +189,6 @@ pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
certificate_configs certificate_configs
} }
pub fn watch_folder(path: String, sender: Sender<Vec<CertificateConfig>>) -> notify::Result<()> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
info!("Watching for certificates in : {}", path);
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
let mut start = Instant::now();
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => match &event.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) | EventKind::Remove(_) => {
if start.elapsed() > Duration::from_secs(1) {
start = Instant::now();
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
info!("Certificate changed: {:?}, {:?}", event.kind, event.paths);
}
}
_ => {}
},
Ok(Err(e)) => error!("Watch error: {:?}", e),
Err(_) => {}
}
}
}
pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Option<String>) { pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Option<String>) {
thread::sleep(time::Duration::from_millis(10)); thread::sleep(time::Duration::from_millis(10));
loop { loop {
@@ -260,9 +213,9 @@ pub fn drop_priv(user: String, group: String, http_addr: String, tls_addr: Optio
} }
pub fn check_priv(addr: &str) { pub fn check_priv(addr: &str) {
let port = SocketAddr::from_str(addr).map(|sa| sa.port()).expect("Failed to parse address port "); let port = SocketAddr::from_str(addr).map(|sa| sa.port()).die("Failed to parse address port ");
if port < 1024 { if port < 1024 {
let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).unwrap(); let meta = std::fs::metadata("/proc/self").map(|m| m.uid()).die("Reading /proc/self");
if meta != 0 { if meta != 0 {
error!("Running on privileged port requires to start as ROOT"); error!("Running on privileged port requires to start as ROOT");
process::exit(1) process::exit(1)
@@ -377,3 +330,75 @@ pub fn write_pid_file(path: &str) -> std::io::Result<()> {
file.write_all(process::id().to_string().as_bytes())?; file.write_all(process::id().to_string().as_bytes())?;
Ok(()) Ok(())
} }
#[allow(clippy::needless_return)]
pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)),
Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr);
return None;
}
},
None => {
if tls {
return Some((addr, 443u16, tls, addr));
} else {
return Some((addr, 80u16, tls, addr));
}
}
};
}
pub fn get_hostname() -> String {
if let Ok(host) = env::var("HOSTNAME").or_else(|_| env::var("COMPUTERNAME")) {
if !host.trim().is_empty() {
return host;
}
}
Command::new("hostname")
.output()
.ok()
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
// pub fn die(message: &str) -> ! {
// log::error!("{message}");
// process::exit(1);
// }
pub trait Die<T> {
fn die(self, message: &str) -> T;
}
impl<T, E: Debug> Die<T> for Result<T, E> {
#[track_caller]
fn die(self, message: &str) -> T {
match self {
Ok(value) => value,
Err(e) => {
let loc = Location::caller();
eprintln!("\x1b[1;31mERROR:\x1b[0m {message}: {e:?}: {}: {}", loc.file(), loc.line());
process::exit(1);
}
}
}
}
impl<T> Die<T> for Option<T> {
#[track_caller]
fn die(self, message: &str) -> T {
match self {
Some(value) => value,
None => {
let loc = Location::caller();
eprintln!("\x1b[1;31mERROR:\x1b[0m {message} at {}:{}", loc.file(), loc.line());
process::exit(1);
}
}
}
}
+40 -16
View File
@@ -1,30 +1,32 @@
use dashmap::DashMap; use dashmap::DashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>; pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>; pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>; pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, Serialize)]
pub struct Extraparams { pub struct Extraparams {
pub to_https: Option<bool>, pub sticky_sessions: u64,
pub sticky_sessions: Option<u64>, pub rate_limit: isize,
pub x4xx_limit: u32,
pub to_https: bool,
pub ssl_verify: bool,
pub authentication: Option<Arc<InnerAuth>>, pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct GlobalServiceMapping { pub struct GlobalServiceMapping {
pub redirect_to: Option<String>,
pub authorization: Option<String>,
pub upstream: String, pub upstream: String,
pub hostname: String, pub hostname: String,
pub path: Option<String>, pub path: Option<String>,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>, pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
@@ -34,14 +36,12 @@ pub struct GlobalServiceMapping {
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes { pub struct Kubernetes {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>, pub tokenpath: Option<String>,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul { pub struct Consul {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>, pub token: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
@@ -92,6 +92,8 @@ pub struct PathConfig {
pub healthcheck: Option<bool>, pub healthcheck: Option<bool>,
pub redirect_to: Option<String>, pub redirect_to: Option<String>,
pub authorization: Option<Auth>, pub authorization: Option<Auth>,
pub is_ssl: Option<bool>,
pub is_http2: Option<bool>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Configuration { pub struct Configuration {
@@ -110,6 +112,8 @@ pub struct AppConfig {
pub hc_method: String, pub hc_method: String,
pub upstreams_conf: String, pub upstreams_conf: String,
pub log_level: String, pub log_level: String,
pub cache_enabled: Option<bool>,
pub cache_storage: Option<String>,
pub cache_size_mb: Option<usize>, pub cache_size_mb: Option<usize>,
pub cache_ttl: Option<u64>, pub cache_ttl: Option<u64>,
pub access_log: Option<String>, pub access_log: Option<String>,
@@ -132,12 +136,17 @@ pub struct AppConfig {
pub runuser: Option<String>, pub runuser: Option<String>,
pub rungroup: Option<String>, pub rungroup: Option<String>,
pub log_file: Option<String>, pub log_file: Option<String>,
pub log_pattern: Option<String>,
pub log_structured: Option<String>,
pub acme_dns_provider: Option<String>,
pub tcp_keepalive_idle: Option<u64>, pub tcp_keepalive_idle: Option<u64>,
pub tcp_keepalive_interval: Option<u64>, pub tcp_keepalive_interval: Option<u64>,
pub tcp_keepalive_count: Option<usize>, pub tcp_keepalive_count: Option<usize>,
pub cluster_member: Option<String>,
pub worker_nodes: Option<String>,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct InnerAuth { pub struct InnerAuth {
pub auth_type: Arc<str>, pub auth_type: Arc<str>,
pub auth_cred: Arc<str>, pub auth_cred: Arc<str>,
@@ -150,13 +159,20 @@ pub struct InnerMap {
pub is_ssl: bool, pub is_ssl: bool,
pub is_http2: bool, pub is_http2: bool,
pub to_https: bool, pub to_https: bool,
pub rate_limit: Option<isize>, pub rate_limit: isize,
pub x4xx_limit: Option<u32>, pub x4xx_limit: u32,
pub healthcheck: Option<bool>, pub healthcheck: bool,
pub redirect_to: Option<Arc<str>>, pub redirect_to: Option<Arc<str>>,
pub authorization: Option<Arc<InnerAuth>>, pub authorization: Option<Arc<InnerAuth>>,
} }
#[allow(dead_code)]
impl Default for InnerMap {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)] #[allow(dead_code)]
impl InnerMap { impl InnerMap {
pub fn new() -> Self { pub fn new() -> Self {
@@ -182,12 +198,20 @@ pub struct InnerMapForJson {
pub is_ssl: bool, pub is_ssl: bool,
pub is_http2: bool, pub is_http2: bool,
pub to_https: bool, pub to_https: bool,
pub rate_limit: Option<isize>, pub rate_limit: isize,
pub x4xx_limit: Option<u32>, pub x4xx_limit: u32,
pub healthcheck: Option<bool>, pub healthcheck: bool,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct UpstreamSnapshotForJson { pub struct UpstreamSnapshotForJson {
pub backends: Vec<InnerMapForJson>, pub backends: Vec<InnerMapForJson>,
pub requests: usize, pub requests: usize,
} }
#[derive(Debug)]
pub struct WorkerNodes {
pub address: Arc<str>,
pub key: Option<Arc<str>>,
pub port: u16,
pub tls: bool,
}
+77
View File
@@ -0,0 +1,77 @@
use crate::tls::CertificateConfig;
use crate::utils::parceyaml::load_configuration;
use crate::utils::tools::{Die, listdir};
use crate::utils::types::Configuration;
use log::{error, info};
use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep;
use std::path::Path;
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
use tokio::sync::mpsc::Sender;
use tokio::task;
pub async fn file_watch(fp: String, toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-)
let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().die("Get parent folder");
let (local_tx, mut local_rx) = tokio::sync::mpsc::channel::<notify::Result<Event>>(1);
let _watcher_handle = task::spawn_blocking({
let parent_dir = parent_dir.to_path_buf(); // Move directory path into the closure
move || {
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = local_tx.blocking_send(res);
},
Config::default(),
)
.die("Initialize file watcher");
watcher.watch(&parent_dir, RecursiveMode::Recursive).die("");
let (_rtx, mut rrx) = tokio::sync::mpsc::channel::<bool>(1);
let _ = rrx.blocking_recv();
}
});
let mut start = Instant::now();
while let Some(event) = local_rx.recv().await {
match event {
Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) if start.elapsed() > Duration::from_secs(2) => {
start = Instant::now();
let snd = load_configuration(file_path, "filepath").await.0;
if let Some(snd) = snd {
toreturn.send(snd).await.die("Send to file watcher");
}
}
_ => (),
},
Err(e) => error!("Watch error: {:?}", e),
}
}
}
pub fn folder_watch(path: String, sender: std::sync::mpsc::Sender<Vec<CertificateConfig>>) -> notify::Result<()> {
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(path.as_ref(), RecursiveMode::Recursive)?;
info!("Watching for certificates in : {}", path);
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
let mut start = Instant::now();
loop {
match rx.recv_timeout(Duration::from_secs(1)) {
Ok(Ok(event)) => match &event.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(_) | EventKind::Remove(_) if start.elapsed() > Duration::from_secs(1) => {
start = Instant::now();
let certificate_configs = listdir(path.clone());
sender.send(certificate_configs)?;
info!("Certificate changed: {:?}, {:?}", event.kind, event.paths);
}
_ => {}
},
Ok(Err(e)) => error!("Watch error: {:?}", e),
Err(_) => {}
}
}
}

Some files were not shown because too many files have changed in this diff Show More