Compare commits

..
88 Commits
Author SHA1 Message Date
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
Ara Sadoyan 29d1f1db78 Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:28:44 +02:00
Ara Sadoyan 3ae54185ad Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:27:12 +02:00
Ara Sadoyan 430f867792 Merge branch 'main' of github.com:sadoyan/aralez 2026-07-20 13:47:54 +02:00
Ara Sadoyan f6c092adc4 version fix 2026-07-20 13:47:05 +02:00
Ara Sadoyan 19ad8fdbf4 yaml update 2026-07-20 13:40:18 +02:00
Ara Sadoyan b55ff2d392 minor fix on memory response cache 2026-07-20 13:37:10 +02:00
Ara Sadoyan b73ccc527f Implementing in memory response cache 2026-07-20 12:25:27 +02:00
Ara Sadoyan c89fe48e44 Cargo.toml version update 2026-07-13 11:35:52 +02:00
Ara Sadoyan d21fd8cb4b config update 2026-07-13 11:32:36 +02:00
Ara Sadoyan 491442bb92 Added log file rotation and compression. Updated to Rust 1.97.0 2026-07-13 11:29:31 +02:00
Ara Sadoyan e07791e1f0 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:47 +02:00
Ara Sadoyan 7b952b467c Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:08 +02:00
Ara Sadoyan 9a1e8fe149 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 12:27:49 +02:00
Ara Sadoyan 874ce4fddb Implementing full async access log inspired by #45 2026-06-29 21:35:10 +02:00
Ara Sadoyan 99c7a811da Header parsing broken with rsplit_once in populate_headers_and_auth and build_headers
closed #48
2026-06-27 21:34:13 +02:00
Ara Sadoyan edadda803c version change 2026-06-27 14:20:06 +02:00
Ara Sadoyan c5d0fe60a0 resolved possible divide by zero issue from #46 2026-06-27 13:38:59 +02:00
Ara Sadoyan d7fe476b24 Merge pull request #47 from sadoyan/dev
Dev
2026-06-27 12:33:32 +02:00
Ara Sadoyan aa22749393 Merge branch 'main' into dev 2026-06-27 12:32:41 +02:00
Ara Sadoyan 4b6a558b4b Merge branch 'main' into dev 2026-06-27 12:31:49 +02:00
Ara Sadoyan 6c9c7acf65 Merge pull request #46 from Taqman-probe/fix/sticky-session-404
Fix: Resolve 404/405 errors with sticky session cookie handling in /api routes
2026-06-27 12:31:29 +02:00
Taqman-probe 2845dc5923 fix 404 error on multipath and cleanup fn: clone_idmap_into 2026-06-27 13:51:12 +09:00
Ara Sadoyan f2e07a8d70 resolved #44 2026-06-26 16:32:58 +02:00
Ara Sadoyan 5815da6576 Merge pull request #43 from sadoyan/dev
Update main.yaml
2026-06-22 13:27:17 +02:00
Ara Sadoyan 1ab0916026 Merge branch 'dev' 2026-06-22 13:24:42 +02:00
Ara Sadoyan 707f725b88 Update main.yaml 2026-06-22 13:22:45 +02:00
Ara Sadoyan eb4e73ece0 Basic access/error logging. Upgrade to Pingra 8.0.1 2026-06-22 13:20:56 +02:00
Ara Sadoyan 53e7dcfd33 Pid file follow symlink problem. 2026-06-19 11:14:40 +02:00
Ara Sadoyan f3b346a28d Merge pull request #42 from sadoyan/dev
migrate from serde_yml to noyalib
2026-06-12 16:05:30 +02:00
Ara Sadoyan c011800e1e migrate from serde_yml to noyalib 2026-06-12 16:03:49 +02:00
Ara Sadoyan 265ff6b774 cleanup 2026-06-12 13:33:21 +02:00
Ara Sadoyan 1c3d9a263f example configs 2026-06-12 13:12:39 +02:00
96 changed files with 6623 additions and 2255 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 }}
+3 -5
View File
@@ -5,12 +5,8 @@
*.dll
*.exe
*.sh
/docs/
*.yaml
/docs
/etc
/etc/
etc
.etc/
assets/
assets
/target/
@@ -25,5 +21,7 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
/target
/.zig-cache/
/z_shpo
/configs
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
+2223 -1084
View File
File diff suppressed because it is too large Load Diff
+44 -26
View File
@@ -1,10 +1,10 @@
[package]
name = "aralez"
version = "0.92.10"
edition = "2021"
version = "0.94.4"
edition = "2024"
license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora"
exclude = ["etc/*"]
exclude = ["configs/*"]
repository = "https://github.com/sadoyan/aralez"
[profile.release]
@@ -15,41 +15,59 @@ panic = "abort"
strip = true
[dependencies]
tokio = { version = "1.52.3", features = ["full"] }
pingora = { version = "0.8.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.9.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.9.0"
pingora-proxy = "0.9.0"
pingora-http = "0.9.0"
pingora-limits = "0.9.0"
pingora-cache = "0.9.0"
dashmap = "7.0.0-rc2"
pingora-core = "0.8.0"
pingora-proxy = "0.8.0"
pingora-http = "0.8.0"
pingora-limits = "0.8.0"
async-trait = "0.1.89"
log = "0.4.30"
futures = "0.3.32"
async-trait = "0.1.92"
log = "0.4.34"
notify = "9.0.0-rc.4"
axum = { version = "0.8.9" }
reqwest = { version = "0.13.4", features = ["json", "stream", "blocking"] }
serde_yml = "0.0.12"
rand = "0.10.1"
base64 = "0.22.1"
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.6"
sha2 = { version = "0.11.0-rc.5", default-features = false }
serde = { version = "1.0.229", features = ["derive"] }
noyalib = { version = "0.0.44", features = ["compat-serde-yaml"] }
rand = "0.10.2"
base64 = "0.23.1"
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["use_pem", "rust_crypto"] }
sha2 = { version = "0.11.0", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3"
arc-swap = "1.9.1"
arc-swap = "1.9.2"
prometheus = "0.14.0"
x509-parser = "0.18.1"
rustls-pemfile = "2.2.0"
tower-http = { version = "0.6.11", features = ["fs"] }
tower-http = { version = "0.7.1", features = ["fs"] }
privdrop = "0.5.6"
serde_json = "1.0.150"
serde_json = "1.0.151"
subtle = "2.6.1"
moka = { version = "0.12.15", features = ["sync"] }
moka = { version = "0.12.16", features = ["sync"] }
ahash = "0.8.12"
instant-acme = "0.8.5"
rcgen = "0.14.8"
log4rs = "1.4.0"
rcgen = "0.14.10"
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 }
signal-hook = "0.4.4"
sd-notify = "0.5.0"
libc = "0.2.186"
tokio-native-tls = "0.3.1"
hyper = "1.11.1"
hyper-util = "0.1.20"
http-body-util = "0.1.5"
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"] }
+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
+50 -50
View File
@@ -6,13 +6,12 @@
### **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,
featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a lightweight ingress-style proxy.
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
lightweight ingress-style proxy.
---
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>.
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>.
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.
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **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.
- **Basic Auth**
- **API Key** via `x-api-key` header
@@ -60,34 +60,36 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers
### `main.yaml`
| Key | Example Value | Description |
|----------------------------------|----------------------------|-------------------------------------------------------------------------------------------------|
| **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) |
| **rungroup** | aralez | Optional. Group for running aralez after dropping root privileges (requires launch as root) |
| **daemon** | false | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
| **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 |
| **proxy_tls_grade** | high, medium, unsafe | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
| **proxy_configs** | /etc/aralez/ | Direcotry containing configuration files, must be writeable by user `aralez` |
| **upstreams_conf** | /etc/aralez/upstreams.yaml | Location of the upstreams file |
| **log_level** | info | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` |
| **log_file** | /full/path/to/aralez.log | Optional, the location of log file. If thi entry does not exist logs will be emitted to stdout. |
| **hc_method** | HEAD | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **file_server_folder** | /some/local/folder | Optional. Local folder to serve |
| **file_server_address** | 127.0.0.1:3002 | Optional. Local address for file server |
| **config_api_enabled** | true | Enable/disable remote config push capability |
| Key | Example Value | Description |
|----------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **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) |
| **rungroup** | aralez | Optional. Group for running aralez after dropping root privileges (requires launch as root) |
| **daemon** | false | Run in background (boolean) |
| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections |
| **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 |
| **proxy_tls_grade** | high, medium, unsafe | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **proxy_address_http** | 0.0.0.0:6193 | Aralez HTTP bind address |
| **proxy_address_tls** | 0.0.0.0:6194 | Aralez HTTPS bind address (Optional) |
| **proxy_configs** | /etc/aralez/ | Direcotry containing configuration files, must be writeable by user `aralez` |
| **upstreams_conf** | /etc/aralez/upstreams.yaml | Location of the upstreams file |
| **access_log** | access | Configure access logging. Values: `access, error` |
| **log_level** | info | Log level: `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. Defaults to None, 100, 5, No |
| **hc_method** | HEAD | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **hc_interval** | 2 | Interval for health checks in seconds |
| **file_server_folder** | /some/local/folder | Optional. Local folder to serve |
| **file_server_address** | 127.0.0.1:3002 | Optional. Local address for file server |
| **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_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. |
---
## Installation
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.
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.
File names:
@@ -105,8 +107,7 @@ File names:
**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.
In general musl builds have a little less performance.
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies. 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.
@@ -296,8 +297,7 @@ openssl req -x509 -newkey rsa:4096 \
## Remote Config API
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates.
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY}
@@ -378,22 +378,22 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | Rust | C | C | Go | Go | C++ |
---
@@ -440,7 +440,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
## 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
- Duration : 10 Minutes
- Binary : aralez-x86_64-glibc
@@ -496,7 +496,7 @@ Error distribution:
![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
- Duration : 10 Minutes
- Binary : aralez-x86_64-musl
@@ -554,7 +554,7 @@ Error distribution:
## 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 .
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"
+19 -14
View File
@@ -1,23 +1,28 @@
# Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting
#runuser: pastor # Username for running aralez after dropping root privileges, requires program to start as root
#rungroup: pastor # Group for running aralez after dropping root privileges, requires program to start as root
#daemon: false # Run in background
runuser: aralez # Username 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
#pid_file: /tmp/aralez.pid # Path to PID file
#error_log: /tmp/aralez_err.log # Path to error log
pid_file: /tmp/aralez.pid # Path to PID file
upgrade_sock: /tmp/aralez.sock # Path to socket file
config_api_enabled: true # Boolean to enable/disable remote config push capability.
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 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
proxy_configs: /opt/Rust/Projects/asyncweb/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
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_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_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /opt/Rust/Projects/asyncweb/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /tmp/gazan # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
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_address: 0.0.0.0:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off
#log_file: /tmp/aralez.log # Optional, the location of log file. If this entry does not exist logs will be emitted to stdout.
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_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 if commented)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds
#master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret
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_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
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
-19
View File
@@ -1,19 +0,0 @@
provider: "file"
globals:
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special"
upstreams:
myip.netangels.net:
paths:
"/":
ssl: false
headers:
- "X-Proxy-From:Aralez"
servers:
- "192.168.221.213:8000"
- "192.168.221.214:8000"
- "192.168.221.210:8000"
- "192.168.221.212:8000"
+37 -94
View File
@@ -4,13 +4,10 @@ sticky_sessions: 172000
to_https: false
rate_limit: 500000
x4xx_limit: 100000
#server_headers:
# - "Y-Global-Something: Yes this is something"
#client_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"
server_headers:
- "Y-Global-Something: Something For Servers"
client_headers:
- "X-Global-Something: Something For Clients"
#authorization:
# type: "basic"
# data: "root:toor"
@@ -38,10 +35,10 @@ kubernetes:
servers:
- "172.16.0.11:5443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
services:
- hostname: "api-service-v2"
upstream: "api-service-v2"
- hostname: "api-service"
upstream: "api-service"
path: "/"
- hostname: "api-service-v2"
- hostname: "api-service"
upstream: "console-service"
path: "/one"
client_headers:
@@ -49,7 +46,7 @@ kubernetes:
- "X-Proxy-From:Aralez"
rate_limit: 100
to_https: false
- hostname: "api-service-v2"
- hostname: "api-service"
upstream: "feed-fanout-service"
path: "/two"
- hostname: "websocket-service"
@@ -57,65 +54,51 @@ kubernetes:
path: "/"
tokenpath: "/opt/Rust/Projects/asyncweb/etc/kubetoken.txt" # Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
upstreams:
myip.netangels.net:
www.example.com:
paths:
"/":
# rate_limit: 50
# x4xx_limit: 100
# to_https: false
# authorization:
# type: "basic"
# data: "root:toor"
rate_limit: 50
x4xx_limit: 100
to_https: false
authorization:
type: "basic"
data: "root:toor"
server_headers:
- "Y-Proxy-Server-Some:Yaaaaaaaaaaaaaaa"
- "Y-Proxy-Server-From:Aralez"
- "Y-Proxy-Server-Vers:Aralez v0.89"
- "Y-Proxy-Server-Vers:Aralez v-xxx"
client_headers:
- "X-Proxy-From:Aralezzzzzzzzzzz"
- "X-Hopar-From:Hopaaaaaaaaaaaar"
- "X-Proxy-Some:X-Proxy-Somebody"
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
- "127.0.0.5:8000"
- "192.168.1.1:8000"
"/ping":
to_https: false
to_https: true
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
"/pong":
to_https: false
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
- "X-Some-Thing:Something Else"
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
"/secret":
authorization:
type: "forward"
data: "http://192.168.1.1:8899/admin/login"
#data: "https://netangels.net/admin/login"
data: "http://127.0.0.1:8899/admin/login"
servers:
- "192.168.1.10:8000"
netangels.net:
- "127.0.0.10:8000"
example.com:
paths:
"/":
redirect_to: "https://www.netangels.net:6194"
redirect_to: "https://www.example.com:443"
servers:
- "192.168.1.1:80"
www.netangels.net:
paths:
"/":
to_https: true
servers:
- "192.168.1.1:80"
apt.netangels.net:
- "127.0.0.1:80"
h2.example.com:
paths:
"/":
server_headers:
@@ -124,9 +107,8 @@ upstreams:
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
rate_limit: 60
x4xx_limit: 30
#authorization:
# type: "jwt"
# data: "SOMETHING"
authorization:
type: "jwt"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
@@ -138,51 +120,12 @@ upstreams:
rate_limit: 4
x4xx_limit: 2
servers:
- "192.168.1.1:8899"
- "127.0.0.1:8899"
"/500":
healthcheck: false
servers:
- "192.168.1.1:8899"
# grafanalocal:
# paths:
# "/":
# healthcheck: false
# servers:
# - "95.211.203.222:443"
# "/.well-known/acme-challenge":
# healthcheck: false
# servers:
# - "127.0.0.1:8001"
localpost:
paths:
"/":
to_https: true
servers:
- "127.0.0.1:9000"
# 192.168.177.2:
# paths:
# "/":
# servers:
# - "127.0.0.1:8000"
ara.matyan.org:
paths:
"/":
servers:
- "127.0.0.1:8000"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:3000"
aro.matyan.org:
paths:
"/":
servers:
- "127.0.0.1:8000"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:3000"
DEFAUwLT:
- "127.0.0.1:8899"
DEFAULT:
paths:
"/":
healthcheck: false
+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::utils::structs::InnerAuth;
use crate::auth::types::AuthValidator;
use crate::utils::tools::split_host_port;
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::upstreams::peer::HttpPeer;
use pingora_http::ResponseHeader;
use pingora_core::prelude::HttpPeer;
use pingora_http::{RequestHeader, ResponseHeader};
use pingora_proxy::Session;
use std::collections::HashMap;
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]
impl AuthValidator for ForwardAuth<'_> {
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 base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use pingora_proxy::Session;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant, SystemTime};
use urlencoding::decode;
#[derive(Debug, Serialize, Deserialize)]
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 {
let key = hash_token(token, secret);
@@ -57,7 +98,7 @@ pub fn check_jwt(token: &str, secret: &str) -> bool {
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) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
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
}
}
}
+24 -13
View File
@@ -1,24 +1,30 @@
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::utils::discovery::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::utils::metrics::calc_cache_metrics;
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use crate::utils::tools::*;
use crate::utils::types::Configuration;
use crate::utils::*;
use crate::web::proxyhttp::LB;
use async_trait::async_trait;
use dashmap::DashMap;
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};
use log::{error, info};
use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
#[async_trait]
impl BackgroundService for LB {
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>
let (mut tx, mut rx) = mpsc::channel::<Configuration>(1);
let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await
@@ -66,24 +72,26 @@ impl BackgroundService for LB {
file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.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 ff = self.ump_full.clone();
let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
drop(tokio::spawn(async move {
healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().unwrap())).await
}));
drop(tokio::spawn(async move { refresh_order(certdir, confdir).await }));
tokio::spawn(async move { 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();
tokio::spawn(async move { refresh_order(certdir, confdir, acme_provider).await });
init_access_logging(self.config.access_log.clone()).await;
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
val = rx.next() => {
val = rx.recv() => {
let start = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
if let Some(ss) = val {
clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst);
@@ -130,6 +138,9 @@ impl BackgroundService for LB {
}
}
}
let end = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let startup = end - start;
info!("Loading upstreams took: {}ms", startup.as_millis());
}
}
}
+25 -19
View File
@@ -1,7 +1,6 @@
use crate::utils::structs::InnerMap;
use crate::web::proxyhttp::LB;
use async_trait::async_trait;
use std::sync::atomic::Ordering;
use crate::core::proxyhttp::LB;
use crate::utils::types::InnerMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone)]
@@ -10,30 +9,38 @@ pub struct GetHostsReturHeaders {
pub server_headers: Option<Vec<(String, Arc<str>)>>,
}
#[async_trait]
pub trait GetHost {
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
// fn get_upstreams(&self) -> Arc<UpstreamsDashMap>;
}
#[async_trait]
impl GetHost for LB {
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if let Some(b) = backend_id {
if let Some(bb) = self.ump_byid.get(b) {
return Some(bb.value().clone());
}
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let b = backend_id?;
let bb = self.ump_byid.get(b)?;
let target = bb.value();
servers.iter().any(|s| s.address == target.address && s.port == target.port).then(|| target.clone())
}
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if servers.is_empty() {
return None;
}
if let Some(target) = self.find_sticky_backend(servers, backend_id) {
return Some(target);
}
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
Some(servers[idx].clone())
}
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let host_entry = self.ump_upst.get(peer)?;
let mut end = path.len();
loop {
let slice = &path[..end];
if let Some(entry) = host_entry.get(slice) {
let (servers, index) = entry.value();
if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
return Some(servers[idx].clone());
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
if let Some(pos) = slice.rfind('/') {
@@ -44,9 +51,8 @@ impl GetHost for LB {
}
if let Some(entry) = host_entry.get("/") {
let (servers, index) = entry.value();
if !servers.is_empty() {
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
return Some(servers[idx].clone());
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
None
-1
View File
@@ -1,4 +1,3 @@
pub mod acme;
pub mod bgservice;
pub mod gethosts;
pub mod proxyhttp;
+100 -78
View File
@@ -1,15 +1,18 @@
use crate::utils::auth::authenticate;
use crate::utils::lazylock::{LOCALHOST, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::auth::authenticate;
use crate::core::gethosts::{GetHost, GetHostsReturHeaders};
use crate::logging::core::access_log;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, MEM_CACHE, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*;
use crate::utils::structs::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::web::gethosts::{GetHost, GetHostsReturHeaders};
use crate::utils::types::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use arc_swap::ArcSwap;
use async_trait::async_trait;
use axum::body::Bytes;
use log::{debug, error, warn};
use pingora::http::{RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*;
use log::error;
use pingora::ErrorSource::Upstream;
use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*;
use pingora_cache::cache_control::CacheControl;
use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable};
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_proxy::{ProxyHttp, Session};
@@ -17,14 +20,10 @@ use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::fmt::Write;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
// static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
// pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
// pub static REQUESTS_4XX: LazyLock<Cache<IpAddr, u32>> = LazyLock::new(|| Cache::builder().time_to_live(Duration::from_secs(1)).build());
// pub static LOCALHOST: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("localhost"));
#[derive(Clone)]
pub struct LB {
pub ump_upst: Arc<UpstreamsDashMap>,
@@ -34,6 +33,7 @@ pub struct LB {
pub server_headers: Arc<Headers>,
pub config: Arc<AppConfig>,
pub extraparams: Arc<ArcSwap<Extraparams>>,
pub cache_enabled: bool,
}
pub struct Context {
@@ -43,9 +43,12 @@ pub struct Context {
upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>,
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]
impl ProxyHttp for LB {
type CTX = Context;
@@ -57,15 +60,56 @@ impl ProxyHttp for LB {
upstream_peer: None,
extraparams: self.extraparams.load(),
client_headers: None,
x4xx_limit: None,
x4xx_limit: 0,
}
}
fn cache_key_callback(&self, session: &Session, ctx: &mut Self::CTX) -> Result<CacheKey> {
let host = ctx.hostname.as_deref().unwrap_or("default");
let path = session.req_header().uri.path();
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()
});
let secondary_key = session.req_header().uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/");
Ok(CacheKey::new(primary_key, secondary_key))
}
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
if self.cache_enabled && session.req_header().method == Method::GET {
if let Some(eviction) = EVICTION.get() {
session.cache.enable(&*MEM_CACHE, Some(eviction), None, Some(&*CACHE_LOCK), None);
}
}
Ok(())
}
fn response_cache_filter(&self, _session: &Session, resp: &ResponseHeader, _ctx: &mut Self::CTX) -> Result<RespCacheable> {
let cc = CacheControl::from_resp_headers(resp);
let defaults = CacheMetaDefaults::new(
|status| {
if status == StatusCode::OK {
Some(Duration::from_secs(*CACHE_TTL.get().unwrap_or(&60)))
} else {
None
}
},
0,
0,
);
Ok(pingora_cache::filters::resp_cacheable(cc.as_ref(), resp.clone(), false, &defaults))
}
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst);
_ctx.hostname = hostname;
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 Ok(cookie_str) = cookies.to_str() {
if let Some(pos) = cookie_str.find("backend_id=") {
@@ -84,41 +128,37 @@ impl ProxyHttp for LB {
None => return Ok(false),
Some(ref innermap) => {
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;
warn!("Forbidden: {:?}, {}", session.client_addr(), session.req_header().uri.path());
return Ok(true);
}
}
if let Some(rate) = innermap.x4xx_limit.or(_ctx.extraparams.x4xx_limit) {
_ctx.x4xx_limit = innermap.x4xx_limit;
let rate = if innermap.rate_limit > 0 { innermap.rate_limit } else { _ctx.extraparams.rate_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());
if let Some(rk) = rate_key {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0);
if count > rate {
if xrate > 0 {
_ctx.x4xx_limit = xrate;
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)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) {
warn!("Limit 4XX: {}-rps exceed on {} from {} path {}", rate, oi, oa, session.req_header().uri.path());
}
return Ok(true);
}
}
}
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?;
if let (Some(oi), Some(oa)) = (&_ctx.hostname, rate_key) {
warn!("Limit: {}-rps exceed on {} from {}", rate, oi, oa);
}
return Ok(true);
}
}
if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path();
@@ -133,7 +173,7 @@ impl ProxyHttp for LB {
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 stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() {
@@ -169,29 +209,14 @@ impl ProxyHttp for LB {
Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
if innermap.is_http2 {
if innermap.is_http2 && !session.is_upgrade_req() {
peer.options.alpn = ALPN::H2;
}
if innermap.is_ssl {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
/*
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 {
if ctx.extraparams.sticky_sessions > 0 {
let mut s = String::with_capacity(64);
write!(
&mut s,
@@ -201,9 +226,9 @@ impl ProxyHttp for LB {
innermap.port,
innermap.is_http2,
innermap.to_https,
innermap.x4xx_limit.unwrap_or_default(),
innermap.rate_limit.unwrap_or_default(),
innermap.healthcheck.unwrap_or_default(),
innermap.x4xx_limit,
innermap.rate_limit,
innermap.healthcheck,
innermap.authorization
)
.unwrap_or(());
@@ -267,7 +292,7 @@ impl ProxyHttp for LB {
Ok(())
}
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 {
let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
existing.value().clone()
@@ -281,29 +306,27 @@ impl ProxyHttp for LB {
REVERSE_STORE.insert(hh.clone(), bid.clone());
hh
};
// let _ = _upstream_response.insert_header("set-cookie", format!("backend_id={}; Path=/; Max-Age=600; HttpOnly; SameSite=Lax", tt));
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
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("; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax");
let _ = _upstream_response.insert_header("set-cookie", buf.as_str());
}
}
if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() {
_upstream_response.append_header(k.clone(), v.as_ref())?;
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 {
for (k, v) in client_headers.iter() {
upstream_response.append_header(k.clone(), v.as_ref())?;
}
}
Ok(())
}
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());
debug!("{}, response code: {response_code}", self.request_summary(session, ctx));
let m = &MetricTypes {
method: session.req_header().method.clone(),
code: session.response_written().map(|resp| resp.status),
@@ -313,14 +336,13 @@ impl ProxyHttp for LB {
};
calc_metrics(m);
ACTIVE_SESSIONS.dec();
if let Some(_) = ctx.x4xx_limit.or(ctx.extraparams.x4xx_limit) {
if 400 <= response_code && response_code <= 499 {
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);
REQUESTS_4XX.insert(ip, current + 1);
}
if ctx.x4xx_limit > 0 && (400..=499).contains(&response_code) {
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);
REQUESTS_4XX.insert(ip, current + 1);
}
}
access_log(response_code, &self.request_summary(session, ctx), session).await;
}
}
+31 -20
View File
@@ -1,36 +1,46 @@
// use rustls::crypto::ring::default_provider;
use crate::core::proxyhttp::LB;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::Extraparams;
use crate::utils::folder_watch;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::tools::*;
use crate::web::proxyhttp::LB;
use crate::utils::types::Extraparams;
use arc_swap::ArcSwap;
use dashmap::DashMap;
use log::info;
use pingora::tls::ssl::{SslAlert, SslRef};
use pingora_core::listeners::tls::TlsSettings;
use pingora_cache::eviction::simple_lru::Manager;
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::server::Server;
use privdrop::reexports::libc::SIGQUIT;
use rustls::crypto::ring::default_provider;
use sd_notify::NotifyState;
use signal_hook::{
consts::{SIGINT, SIGTERM},
iterator::Signals,
};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use std::{fs, process, thread};
use std::{fs, thread};
pub fn run() {
// default_provider().install_default().expect("Failed to install rustls crypto provider");
default_provider().install_default().expect("Failed to install RusTLS crypto provider");
let parameters = Opt::parse_args();
let file = parameters.conf.clone().unwrap();
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
let cache_enabled = maincfg.cache_size_mb.is_some();
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();
server.bootstrap();
@@ -41,11 +51,12 @@ pub fn run() {
let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
to_https: None,
sticky_sessions: None,
to_https: false,
ssl_verify: false,
sticky_sessions: 0,
authentication: None,
rate_limit: None,
x4xx_limit: None,
rate_limit: 0,
x4xx_limit: 0,
}));
let cfg = Arc::new(maincfg);
@@ -58,6 +69,7 @@ pub fn run() {
client_headers: ch_config,
server_headers: sh_config,
extraparams: ec_config,
cache_enabled,
};
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
@@ -71,9 +83,6 @@ pub fn run() {
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;
if let Some(idle) = cfg.tcp_keepalive_idle {
let mut to = TcpSocketOptions::default();
@@ -101,7 +110,7 @@ pub fn run() {
fs::create_dir_all(certs_path.clone()).unwrap();
}
thread::spawn(move || {
watch_folder(certs_path, tx).unwrap();
folder_watch(certs_path, tx).unwrap();
});
let certificate_configs = rx.recv().unwrap();
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
@@ -146,10 +155,13 @@ pub fn run() {
drop_priv(user, group, cfg.proxy_address_http.clone(), cfg.proxy_address_tls.clone());
}
let _ = sd_notify::notify(&[NotifyState::Ready]);
let _ = fs::write(cfg.pid_file.clone().unwrap_or("/tmp/aralez.pid".to_string()), process::id().to_string());
let mut signals = Signals::new(&[SIGINT, SIGTERM, SIGQUIT]).unwrap();
for sig in signals.forever() {
let pf = cfg.pid_file.clone().unwrap_or("/tmp/aralez.pid".to_string());
if let Err(e) = write_pid_file(pf.as_str()) {
panic!("Failed to write PID file: {} : {}", pf, e);
}
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT]).unwrap();
if let Some(sig) = signals.forever().next() {
match sig {
SIGINT => info!("SIGINT received! Exiting..."),
SIGTERM => info!("SIGTERM received! Exiting..."),
@@ -159,6 +171,5 @@ pub fn run() {
}
_ => unreachable!(),
}
break;
}
}
+32 -28
View File
@@ -1,20 +1,18 @@
use crate::utils::discovery::APIUpstreamProvider;
use crate::utils::jwt::Claims;
use crate::utils::metrics::{get_memory_usage, get_open_files, MEMORY_USAGE, OPEN_FILES};
use crate::utils::structs::{Config, Configuration, UpstreamsDashMap};
use crate::auth::jwt::Claims;
use crate::ingress::APIUpstreamProvider;
use crate::tls::acme::http01::{acme_create, acme_order, http01_challenge};
use crate::utils::metrics::{MEMORY_USAGE, OPEN_FILES, get_memory_usage, get_open_files};
use crate::utils::tools::{upstreams_liveness_json, upstreams_to_json};
use crate::web::acme::{acme_create, acme_order, http01_challenge};
use crate::utils::types::{Config, Configuration, UpstreamsDashMap};
use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{any, get, post};
use axum::{Json, Router};
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use jsonwebtoken::{encode, EncodingKey, Header};
use jsonwebtoken::{EncodingKey, Header, encode};
use log::{debug, error, info, warn};
use prometheus::{gather, Encoder, TextEncoder};
use prometheus::{Encoder, TextEncoder, gather};
use serde::Serialize;
use signal_hook::{consts::SIGQUIT, iterator::Signals};
use std::collections::HashMap;
@@ -32,19 +30,20 @@ struct OutToken {
}
#[derive(Clone)]
pub(crate) struct AppState {
pub struct AppState {
master_key: Option<String>,
pub(crate) cert_creds: String,
pub(crate) certs_dir: String,
pub acme_dns_provider: Option<String>,
pub cert_creds: String,
pub certs_dir: String,
upstreams_file: String,
config_sender: Sender<Configuration>,
config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>,
full_upstreams: Arc<UpstreamsDashMap>,
}
#[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState {
master_key: config.masterkey.clone(),
@@ -55,6 +54,7 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
acme_dns_provider: config.acme_dns_provider.clone(),
};
let app = Router::new()
// .route("/{*wildcard}", get(senderror))
@@ -69,11 +69,17 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
let mut static_handle: Option<tokio::task::JoinHandle<()>> = None;
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_serve: Router = Router::new().fallback_service(static_files);
// drop(tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() }));
static_handle = Some(tokio::spawn(async move { axum::serve(static_listen, static_serve).await.unwrap() }))
info!("Starting file server on: {}", address);
static_handle = Some(tokio::spawn(async move {
match axum::serve(static_listen, static_serve).await {
Ok(o) => info!("File server exited normally: {:?}", o),
Err(e) => error!("File server exited: {}", e),
}
}))
}
let listener = port_is_available("Config API", &config.address).await;
@@ -82,10 +88,9 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender<Conf
let (tx, mut rx) = mpsc::channel(1);
std::thread::spawn(move || {
let mut signals = Signals::new(&[SIGQUIT]).unwrap();
for sig in signals.forever() {
let mut signals = Signals::new([SIGQUIT]).unwrap();
if let Some(sig) = signals.forever().next() {
tx.blocking_send(sig).unwrap();
break;
}
});
rx.recv().await;
@@ -102,13 +107,13 @@ async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, S
}
let strcontent = content.as_str();
let parsed = serde_yml::from_str::<Config>(strcontent);
let parsed = noyalib::from_str::<Config>(strcontent);
match parsed {
Ok(_) => {
if let Some(_) = params.get("save") {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, true).await }));
if params.contains_key("save") {
tokio::spawn(async move { apply_config(content.as_str(), st, true).await });
} else {
drop(tokio::spawn(async move { apply_config(content.as_str(), st, false).await }));
tokio::spawn(async move { apply_config(content.as_str(), st, false).await });
}
Response::builder().status(StatusCode::OK).body(Body::from("Accepted! Applying in background\n")).unwrap()
}
@@ -119,7 +124,7 @@ async fn conf(State(st): State<AppState>, Query(params): Query<HashMap<String, S
}
}
async fn apply_config(content: &str, mut st: AppState, save: bool) {
async fn apply_config(content: &str, st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 {
if save {
@@ -211,7 +216,7 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(j))
.unwrap()
.unwrap();
}
Err(e) => {
return Response::builder()
@@ -228,8 +233,7 @@ async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String,
}
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);
//if addr.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED) {
+92
View File
@@ -0,0 +1,92 @@
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{list_to_upstreams, ServiceDiscovery};
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 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).unwrap().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,15 +1,18 @@
use crate::utils::filewatch;
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery};
use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver;
use crate::core::webserver;
use crate::ingress::consul::ConsulDiscovery;
use crate::ingress::kuberconsul::ServiceDiscovery;
use crate::ingress::kubernetes::KubernetesDiscovery;
use crate::utils::types::{Configuration, UpstreamsDashMap};
use crate::utils::watch;
use async_trait::async_trait;
use futures::channel::mpsc::Sender;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
pub struct APIUpstreamProvider {
pub config_api_enabled: bool,
pub address: String,
pub masterkey: Option<String>,
pub acme_dns_provider: Option<String>,
pub certs_dir: String,
pub config_dir: String,
pub upstreams_file: String,
@@ -46,7 +49,7 @@ impl Discovery for APIUpstreamProvider {
#[async_trait]
impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx.clone()));
tokio::spawn(watch::file_watch(self.path.clone(), tx));
}
}
+63
View File
@@ -0,0 +1,63 @@
use crate::utils::tools::{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::atomic::AtomicUsize;
use std::sync::Arc;
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.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 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
}
+164
View File
@@ -0,0 +1,164 @@
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::{clone_compare, list_to_upstreams, ServiceDiscovery};
use crate::ingress::kubewatcher::{start_ingress_watcher_with_config, AralezRouteUpdate};
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
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).unwrap().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().expect("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::*;
+6
View File
@@ -0,0 +1,6 @@
pub mod auth;
pub mod core;
pub mod ingress;
pub mod logging;
pub mod tls;
pub mod utils;
+336
View File
@@ -0,0 +1,336 @@
use crate::logging::types::start_logging_backend;
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::types::AppConfig;
use anyhow::Result;
use log::{error, info, log, Level, LevelFilter, Record};
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::Append;
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)).unwrap();
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).unwrap();
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))
.unwrap();
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))
.unwrap();
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;
mod utils;
mod web;
use aralez::core;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@@ -12,5 +10,5 @@ fn main() {
std::process::exit(0);
}
web::start::run();
core::start::run();
}
-2
View File
@@ -1,2 +0,0 @@
pub mod account;
pub mod order;
+74
View File
@@ -0,0 +1,74 @@
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
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").expect("Environment variable CLOUDFLARE_API_TOKEN is required");
let zone_id = std::env::var("CLOUDFLARE_ZONE_ID").expect("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()),
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cloudflare;
pub mod route53;
+123
View File
@@ -0,0 +1,123 @@
// ------------------------ //
// This file is vibecoded !
// ------------------------ //
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use aws_config::SdkConfig;
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").expect("Environment variable AWS_HOSTED_ZONE_ID is required");
// Load AWS SDK config synchronously using Tokio runtime handle to fit factory construction
let config: SdkConfig = if let Ok(handle) = Handle::try_current() {
tokio::task::block_in_place(|| handle.block_on(aws_config::load_from_env()))
} else {
tokio::runtime::Runtime::new().unwrap().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()),
}
}
+9 -12
View File
@@ -1,53 +1,50 @@
use crate::tls::acme::order::CHALLENGES;
use crate::core::webserver;
use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body;
use axum::extract::State;
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
#[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 {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
.unwrap();
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to create account: {}", e)))
.unwrap()
.unwrap();
}
};
}
#[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('/');
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) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap()
.unwrap();
}
Err(e) => {
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Failed to order a certificate: {}", e)))
.unwrap()
.unwrap();
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) {
return Response::builder()
.status(StatusCode::OK)
+45
View File
@@ -0,0 +1,45 @@
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()
.expect("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::*;
+50 -22
View File
@@ -1,36 +1,34 @@
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 base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info};
use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap;
use sha2::{Digest, Sha256};
use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration;
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) {
pub async fn refresh_order(certs_dir: String, autoconf_dir: String, provider: Option<String>) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
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;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<String, Box<dyn std::error::Error>> {
let crt = certs_dir.clone() + "/" + domain + ".crt";
let key = certs_dir.clone() + "/" + domain + ".key";
pub async fn order(domain: &str, credsfile: &str, certs_dir: String, provider: Option<String>) -> Result<String, Box<dyn std::error::Error>> {
let crt = format!("{}/{}.crt", certs_dir, domain);
let key = format!("{}/{}.key", certs_dir, domain);
if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true);
let mut newlist: Vec<String> = Vec::new();
for item in DOMAINS.iter() {
newlist.push(item.key().to_string());
}
let newlist: Vec<String> = DOMAINS.iter().map(|item| item.key().to_string()).collect();
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = std::fs::write(&autocfg_file, json_content) {
@@ -43,7 +41,6 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
// println!("Fresh certificate exists. Not renewing !");
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
};
@@ -52,14 +49,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 authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
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_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);
challenge_handle.set_ready().await?;
if is_dns {
let prov = provider.as_ref().unwrap();
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().unwrap().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?;
@@ -71,12 +97,9 @@ pub async fn order(domain: &str, credsfile: &str, certs_dir: String) -> Result<S
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?;
// poll for certificate
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().unwrap().clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
@@ -89,3 +112,8 @@ fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let expiry = cert.validity().not_after.timestamp() as u64;
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 {
pub fn from_str(s: &str) -> Option<Self> {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High),
"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) {
let config_grade = TlsGrade::from_str(grade);
let config_grade = TlsGrade::parse(grade);
match config_grade {
Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
+1 -1
View File
@@ -54,7 +54,7 @@ impl Certificates {
cert_infos.push(cert)
}
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;
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod acme;
pub mod grades;
pub mod load;
pub use load::*;
-53
View File
@@ -1,53 +0,0 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
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::task;
pub async fn start(fp: String, mut 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);
#[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout)
@@ -23,9 +24,9 @@ unsafe impl GlobalAlloc for CountingAllocator {
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
#[allow(dead_code)]
fn for_example() {
let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
println!("Allocations : {}", after - before);
}
// #[allow(dead_code)]
// fn for_example() {
// let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// println!("Allocations : {}", after - before);
// }
+23
View File
@@ -0,0 +1,23 @@
use bytes::Bytes;
use http_body_util::Full;
use hyper::Request;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::TokioExecutor;
pub async fn ping_grpc(addr: &str) -> bool {
let connector = HttpConnector::new();
let client = Client::builder(TokioExecutor::new()).http2_only(true).build(connector);
let uri = format!("{}{}", addr, "/aralez.Probe/Ping");
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 {
Ok(resp) => resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/grpc"))
.unwrap_or(false),
Err(_) => false,
}
}
+197
View File
@@ -0,0 +1,197 @@
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 async fn httpclient(req: HttpClientRequest<'_>) -> (bool, bool) {
let HttpClientRequest {
method,
tls,
host,
path,
address,
port,
url,
payload,
} = 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);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url).await {
return (true, true);
}
}
}
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);
}
};
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();
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);
}
if let Err(e) = http_session.write_request_body(body_bytes, true).await {
log::warn!("Write body failed: {}", e);
return (false, false);
}
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);
}
log::warn!("Health Check read failed ({}) : {} - {}", if is_h2 { "H2" } else { "H1" }, host, e);
return (false, false);
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2)
}
/*
pub async fn httpclient(method: &str, tls: bool, host: &str, path: &str, address: &str, port: u16, url: String) -> (bool, bool) {
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);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url.as_str()).await {
return (true, true);
}
}
}
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);
}
};
req.insert_header("Host", host).ok();
if let Err(e) = http_session.write_request_header(Box::new(req)).await {
log::warn!("Write header failed: {}", e);
return (false, false);
}
let status = if is_h2 {
if let Err(e) = http_session.write_request_body(Bytes::new(), true).await {
log::warn!("Write body failed: {}", e);
return (false, false);
}
match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
if ping_grpc(url.as_str()).await {
return (true, true);
}
log::warn!("Health Check H2 read failed: {} - {}", host, e);
return (false, false);
}
}
} else {
match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("Health Check H1 read failed: {} - {}", e, host);
return (false, false);
}
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2)
}
*/
+44 -82
View File
@@ -1,23 +1,23 @@
use crate::utils::hcclient::{HttpClientRequest, httpclient};
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tools::*;
use crate::utils::types::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use bytes::Bytes;
use dashmap::DashMap;
use log::{error, warn};
use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::time::interval;
use tonic::transport::Endpoint;
use tokio::net::TcpStream;
use tokio::time::{interval, timeout};
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)) {
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();
loop {
tokio::select! {
_ = period.tick() => {
// populate_upstreams(&upslist, &fullist, &idlist, params, &client).await;
let totest = build_upstreams(&fullist, params.0, &client).await;
let totest = build_upstreams(&fullist, params.0).await;
if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist);
@@ -28,22 +28,11 @@ pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>,
}
}
/*
pub async fn populate_upstreams(upslist: &Arc<UpstreamsDashMap>, fullist: &Arc<UpstreamsDashMap>, idlist: &Arc<UpstreamsIdMap>, params: (&str, u64), client: &Client) {
let totest = build_upstreams(fullist, params.0, client).await;
if !compare_dashmaps(&totest, upslist) {
clone_dashmap_into(&totest, upslist);
clone_idmap_into(&totest, idlist);
}
}
*/
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
build_upstreams(&fullist, "HEAD").await
}
async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Client) -> UpstreamsDashMap {
async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str) -> UpstreamsDashMap {
let totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist);
for val in fclone.iter() {
@@ -55,19 +44,16 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
let mut innervec = Vec::new();
for upstream in path_entry.value().0.iter() {
let tls = if upstream.healthcheck.unwrap_or(true) {
detect_tls(upstream.address.as_ref(), &upstream.port, client).await
let tls = if upstream.healthcheck {
detect_tls(upstream.address.as_ref(), &upstream.port, host).await
} else {
(false, None)
false
};
let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let mut scheme = InnerMap {
address: upstream.address.clone(),
port: upstream.port,
is_ssl: tls.0,
is_http2: is_h2,
is_ssl: tls,
is_http2: false,
to_https: upstream.to_https,
rate_limit: upstream.rate_limit,
x4xx_limit: upstream.x4xx_limit,
@@ -76,21 +62,28 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
authorization: upstream.authorization.clone(),
};
if scheme.healthcheck.unwrap_or(true) {
let link = if tls.0 {
if scheme.healthcheck {
let link = if tls {
format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else {
format!("http://{}:{}{}", upstream.address, upstream.port, path)
};
let resp = http_request(&link, method, "", client).await;
let resp = httpclient(HttpClientRequest {
method,
tls,
host,
path,
address: upstream.address.as_ref(),
port: upstream.port,
url: link.as_str(),
payload: EMPTY_PAYLOAD,
})
.await;
if resp.0 {
if resp.1 {
scheme.is_http2 = is_h2; // could be adjusted further
scheme.is_http2 = resp.1;
}
innervec.push(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
}
} else {
innervec.push(Arc::from(scheme));
@@ -103,51 +96,20 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
totest
}
async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -> (bool, bool) {
if !["POST", "GET", "HEAD"].contains(&method) {
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method);
return (false, false);
}
async fn send_request(client: &Client, method: &str, url: &str, payload: &str) -> Option<reqwest::Response> {
match method {
"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),
}
}
pub async fn ping_grpc(addr: &str) -> bool {
let endpoint = match Endpoint::from_shared(addr.to_owned()) {
Ok(e) => e.timeout(Duration::from_secs(2)),
async fn detect_tls(ip: &str, port: &u16, sni: &str) -> bool {
let addr = format!("{ip}:{port}");
let timeout_duration = Duration::from_secs(2); // THINK !!!
let stream = match timeout(timeout_duration, TcpStream::connect(&addr)).await {
Ok(Ok(s)) => s,
Ok(Err(_)) => return false,
Err(_) => return false,
};
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some()
}
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) {
let https_url = format!("https://{}:{}", ip, port);
if let Ok(response) = client.get(&https_url).send().await {
return (true, Some(response.version()));
}
let http_url = format!("http://{}:{}", ip, port);
match client.get(&http_url).send().await {
Ok(response) => (false, Some(response.version())),
Err(_) => {
if ping_grpc(&http_url).await {
(false, Some(Version::HTTP_2))
} else {
(false, None)
}
}
}
let connector = tokio_native_tls::TlsConnector::from(
TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.unwrap(),
);
connector.connect(sni, stream).await.is_ok()
}
+223 -68
View File
@@ -1,85 +1,240 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints};
use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use axum::http::{HeaderMap, HeaderValue};
use crate::ingress::consul::ConsulService;
use crate::ingress::kuberconsul::match_path;
use crate::ingress::kubernetes::KubeEndpointSliceList;
use crate::utils::types::{GlobalServiceMapping, InnerMap};
use ahash::HashMap;
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::Arc;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap());
}
let to = Duration::from_secs(1);
let resp = client.get(url).timeout(to).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Consul API returned status: {}", resp.status());
pub static CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct ConsulServicesInternal {
#[serde(rename = "aralez.host")]
pub host: String,
#[serde(rename = "aralez.path")]
pub path: String,
#[serde(rename = "aralez.auth")]
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;
}
let mut inner_vec = Vec::new();
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let endpoints: Vec<ConsulService> = resp.json().await.ok()?;
for subsets in endpoints {
let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone();
let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port;
let to_add = Arc::from(InnerMap {
address: Arc::from(&*addr),
port: prt,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
redirect_to: None,
healthcheck: None,
authorization: None,
});
inner_vec.push(to_add);
}
match_path(conf, &upstreams, inner_vec);
Some(upstreams)
None
}
fn parse_services(json: Vec<u8>) -> Result<ConsulServices, serde_json::Error> {
let raw: HashMap<String, Vec<String>> = serde_json::from_slice(&json)?;
Ok(raw
.into_iter()
.map(|(service, tags)| {
let mut internal = ConsulServicesInternal::default();
for tag in tags {
if let Some((key, value)) = tag.split_once('=') {
match key {
"aralez.host" => internal.host = value.to_string(),
"aralez.path" => internal.path = value.to_string(),
"aralez.rate" => internal.rate = value.parse::<isize>().ok(),
"aralez.4xx_rate" => internal.xrate = value.parse::<u32>().ok(),
"aralez.to_https" => internal.to_https = value.parse::<bool>().ok(),
"aralez.auth" => internal.auth = Option::from(value.to_string()),
"aralez.redirect" => internal.redirect = Option::from(value.to_string()),
"aralez.client_header" => internal.client_headers.get_or_insert_default().push(value.to_string()),
"aralez.server_header" => internal.server_headers.get_or_insert_default().push(value.to_string()),
_ => {}
}
}
}
(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)>> {
let to = Duration::from_secs(10);
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?;
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?;
if !resp.status().is_success() {
eprintln!("Kubernetes API returned status: {}", resp.status());
return None;
}
let endpoints: KubeEndpoints = resp.json().await.ok()?;
if let Some(data) = getfromapi(url, Some(token.to_string()), "kubernetes").await {
let slice_list: KubeEndpointSliceList = serde_json::from_slice(&data).ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
let mut inner_vec = Vec::new();
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 subset in subsets {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
let mut inner_vec = Vec::new();
for addr in addresses {
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 {
address: Arc::from(addr.ip.clone()),
port: port.port,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
healthcheck: None,
redirect_to: None,
authorization: None,
});
inner_vec.push(to_add);
for ep in &slice.endpoints {
let is_ready = ep.conditions.as_ref().and_then(|c| c.ready).unwrap_or(true);
if !is_ready {
continue;
}
for addr in &ep.addresses {
for port in ports {
if let Some(port_num) = port.port {
let to_add = Arc::from(InnerMap {
address: Arc::from(addr.as_str()),
port: port_num,
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),
healthcheck: true,
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))
}
-229
View File
@@ -1,229 +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 futures::channel::mpsc::Sender;
use futures::SinkExt;
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;
#[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>, mut 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>, mut 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
View File
@@ -1,11 +1,22 @@
use dashmap::DashMap;
use moka::sync::Cache;
use pingora_cache::MemCache;
use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::lock::CacheLock;
use pingora_limits::rate::Rate;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, LazyLock};
use std::sync::{OnceLock, RwLock};
use std::time::Duration;
pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
pub static RATE_LIMITER: LazyLock<Rate> = LazyLock::new(|| Rate::new(Duration::from_secs(1)));
pub static REQUESTS_4XX: LazyLock<Cache<IpAddr, u32>> = LazyLock::new(|| Cache::builder().time_to_live(Duration::from_secs(1)).build());
pub static LOCALHOST: LazyLock<Arc<str>> = LazyLock::new(|| Arc::from("localhost"));
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 EVICTION: OnceLock<Manager> = 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);
+84 -18
View File
@@ -1,3 +1,5 @@
use crate::utils::lazylock::EVICTION;
use pingora_cache::eviction::EvictionManager;
use pingora_http::Method;
use pingora_http::StatusCode;
use pingora_http::Version;
@@ -15,22 +17,19 @@ pub struct MetricTypes {
}
pub static OPEN_FILES: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_open_files", "Number of open file descriptors").unwrap());
pub static LOGGING_ERRORS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_logging_errors", "Number of log errors").unwrap());
pub static MEMORY_USAGE: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_memory_bytes", "Total memory allocated in bytes").unwrap());
pub static ACTIVE_SESSIONS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_active_sessions", "Current number of active sessions").unwrap());
pub static REQUEST_COUNT: LazyLock<IntCounter> = LazyLock::new(|| register_int_counter!("aralez_requests_total", "Total number of requests handled by Aralez").unwrap());
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_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_items", "Current number of cached objects").unwrap());
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_EVICTED_ITEMS: LazyLock<IntGauge> = LazyLock::new(|| register_int_gauge!("aralez_cache_evicted_items_total", "Total cache items evicted").unwrap());
pub static RESPONSE_CODES: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_responses_total", "Responses grouped by status code", &["status"]).unwrap());
// pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
// register_histogram!(
// "aralez_response_latency_seconds",
// "Response latency in seconds",
// vec![0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.0, 5.0]
// )
// .unwrap()
// });
pub static RESPONSE_LATENCY: LazyLock<Histogram> = LazyLock::new(|| {
register_histogram!(
"aralez_response_latency_seconds",
@@ -48,19 +47,74 @@ pub static REQUESTS_BY_UPSTREAM: LazyLock<IntCounterVec> =
pub static REQUESTS_BY_VERSION: LazyLock<IntCounterVec> =
LazyLock::new(|| register_int_counter_vec!("aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"]).unwrap());
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) {
REQUEST_COUNT.inc();
let version_str = match metric_types.version {
Version::HTTP_11 => "HTTP/1.1",
Version::HTTP_2 => "HTTP/2.0",
Version::HTTP_3 => "HTTP/3.0",
Version::HTTP_10 => "HTTP/1.0",
_ => "Unknown",
match metric_types.version {
Version::HTTP_11 => HTTP_11_COUNT.inc(),
Version::HTTP_2 => HTTP_20_COUNT.inc(),
Version::HTTP_3 => HTTP_30_COUNT.inc(),
Version::HTTP_10 => HTTP_10_COUNT.inc(),
_ => HTTP_UNK_COUNT.inc(),
};
REQUESTS_BY_VERSION.with_label_values(&[version_str]).inc();
RESPONSE_CODES.with_label_values(&[metric_types.code.unwrap_or(StatusCode::GONE).as_str()]).inc();
REQUESTS_BY_METHOD.with_label_values(&[metric_types.method.as_str()]).inc();
match metric_types.method.as_str() {
"GET" => METHOD_GET.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();
RESPONSE_LATENCY.observe(metric_types.latency.as_secs_f64());
}
@@ -81,3 +135,15 @@ pub(crate) fn get_memory_usage() -> usize {
pub fn get_open_files() -> usize {
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;
}
}
+6 -7
View File
@@ -1,15 +1,14 @@
pub mod auth;
pub mod discovery;
mod filewatch;
pub mod fordebug;
pub mod grpc;
pub mod hcclient;
pub mod healthcheck;
pub mod httpclient;
pub mod jwt;
pub mod kuberconsul;
// pub mod ingress;
pub mod lazylock;
pub mod metrics;
pub mod parceyaml;
pub mod state;
pub mod structs;
pub mod tools;
// pub mod watchksecret;
pub mod types;
pub(crate) mod watch;
pub use watch::*;
+20 -66
View File
@@ -1,24 +1,16 @@
use crate::utils::healthcheck;
use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::lazylock::{DOMAINS, REVERSE_STORE};
use crate::utils::state::{is_first_run, mark_not_first_run};
use crate::utils::structs::*;
use crate::utils::tools::{clone_dashmap, clone_dashmap_into, print_upstreams};
use crate::utils::types::*;
use dashmap::DashMap;
use log::LevelFilter;
use log::{error, info, warn};
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, LazyLock};
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) {
let mut conf_files = Vec::new();
let yaml_data = match kind {
@@ -108,7 +100,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
}
};
let mut parsed: Config = match serde_yml::from_str(&yaml_data) {
let mut parsed: Config = match noyalib::from_str(&yaml_data) {
Ok(cfg) => cfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
@@ -118,7 +110,7 @@ pub async fn load_configuration(d: &str, kind: &str) -> (Option<Configuration>,
if let Some(ref mut upstreams) = parsed.upstreams {
for uconf in conf_files {
let p: HashMap<String, HostConfig> = match serde_yml::from_str(&uconf) {
let p: HashMap<String, HostConfig> = match noyalib::from_str(&uconf) {
Ok(ucfg) => ucfg,
Err(e) => {
error!("Failed to parse upstreams file: {}", e);
@@ -178,10 +170,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();
server_global_headers.insert(Arc::from("/"), sh);
config.server_headers.insert(Arc::from("GLOBAL_SERVER_HEADERS"), server_global_headers);
config.extraparams.to_https = parsed.to_https;
config.extraparams.sticky_sessions = parsed.sticky_sessions;
config.extraparams.rate_limit = parsed.rate_limit;
config.extraparams.x4xx_limit = parsed.x4xx_limit;
config.extraparams.to_https = parsed.to_https.unwrap_or(false);
config.extraparams.sticky_sessions = parsed.sticky_sessions.unwrap_or(0);
config.extraparams.rate_limit = parsed.rate_limit.unwrap_or(0);
config.extraparams.x4xx_limit = parsed.x4xx_limit.unwrap_or(0);
if let Some(rate) = &parsed.rate_limit {
info!("Applied Global Rate Limit : {} request per second", rate);
@@ -225,7 +217,8 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
}
let redirect_link = path_config.redirect_to.as_ref().map(|www| Arc::from(www.as_str()));
if let Some((ip, port_str)) = server.split_once(':') {
if let Some((ip, port_str)) = server.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
server_list.push(Arc::from(InnerMap {
address: Arc::from(ip),
@@ -233,9 +226,9 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
is_ssl: false,
is_http2: false,
to_https: path_config.to_https.unwrap_or(false),
rate_limit: path_config.rate_limit,
x4xx_limit: path_config.x4xx_limit,
healthcheck: path_config.healthcheck,
rate_limit: path_config.rate_limit.unwrap_or(0),
x4xx_limit: path_config.x4xx_limit.unwrap_or(0),
healthcheck: path_config.healthcheck.unwrap_or(true),
redirect_to: redirect_link,
authorization: path_auth,
}));
@@ -263,32 +256,25 @@ async fn populate_file_upstreams(config: &mut Configuration, parsed: &Config) {
}
}
pub fn parce_main_config(path: &str) -> AppConfig {
let data = fs::read_to_string(path).unwrap();
let reply = DashMap::new();
let cfg: HashMap<String, String> = serde_yml::from_str(&data).expect("Failed to parse main config file");
let mut cfo: AppConfig = serde_yml::from_str(&data).expect("Failed to parse main config file");
let data = fs::read_to_string(path).expect("Failed to read main config file");
let mut cfo: AppConfig = noyalib::from_str(&data).expect("Failed to parse main config file");
if let Ok(jwt_key) = env::var("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();
for (k, v) in cfg {
reply.insert(k.to_string(), v.to_string());
}
if let Some((ip, port_str)) = cfo.config_address.split_once(':') {
if let Some((ip, port_str)) = cfo.config_address.rsplit_once(':') {
if let Ok(port) = port_str.parse::<u16>() {
cfo.local_server = Option::from((ip.to_string(), port));
}
}
if let Some(tlsport_cfg) = cfo.proxy_address_tls.clone() {
if let Some((_, port_str)) = tlsport_cfg.split_once(':') {
if let Some((_, port_str)) = tlsport_cfg.rsplit_once(':') {
cfo.proxy_port_tls = Some(port_str.to_string());
}
};
if let Some((_, port_str)) = cfo.proxy_address_http.split_once(':') {
if let Some((_, port_str)) = cfo.proxy_address_http.rsplit_once(':') {
cfo.proxy_port = Some(port_str.to_string());
}
@@ -332,35 +318,3 @@ pub fn build_headers(path_config: &Option<Vec<String>>, _config: &Configuration,
}
}
}
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 pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}{n}";
let pattern = "{d(%Y-%m-%d %H:%M:%S)} {l} {t} - {m}\n";
if let Some(location) = location {
let file = FileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(location).unwrap();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.build(Root::builder().appender("stdout").build(log_level))
.unwrap();
log4rs::init_config(config).unwrap();
}
}
-11
View File
@@ -16,14 +16,3 @@ pub fn is_first_run() -> bool {
let state = GLOBAL_STATE.read().unwrap();
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
}
}
*/
+59 -75
View File
@@ -1,24 +1,24 @@
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::structs::{Extraparams, InnerMap, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::types::{Extraparams, InnerMapForJson, UpstreamSnapshotForJson, UpstreamsDashMap, UpstreamsIdMap};
use dashmap::DashMap;
use log::{error, info};
use notify::{event::ModifyKind, Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use privdrop::PrivDrop;
use serde_json::{json, Value};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::any::type_name;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
use std::fs::OpenOptions;
use std::io::Write as IoWrite;
use std::net::SocketAddr;
use std::net::TcpListener;
use std::os::unix::fs::MetadataExt;
use std::os::unix::fs::OpenOptionsExt;
use std::process::Command;
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::{fs, process, thread, time};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{env, fs, process, thread, time};
pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams) {
let mut out = String::new();
@@ -35,8 +35,8 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams)
f.is_ssl,
f.is_http2,
f.to_https,
f.rate_limit.unwrap_or(extraparams.rate_limit.unwrap_or(0)),
f.x4xx_limit.unwrap_or(extraparams.x4xx_limit.unwrap_or(0))
if f.rate_limit > 0 { f.rate_limit } else { extraparams.rate_limit },
if f.x4xx_limit > 0 { f.x4xx_limit } else { extraparams.x4xx_limit }
)
.unwrap();
}
@@ -44,24 +44,6 @@ pub fn print_upstreams(upstreams: &UpstreamsDashMap, extraparams: &Extraparams)
}
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 {
let new_map: UpstreamsDashMap = DashMap::new();
@@ -144,11 +126,8 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
cloned.clear();
for outer_entry in original.iter() {
let inner_map = outer_entry.value();
let new_inner_map = DashMap::new();
for inner_entry in inner_map.iter() {
let path = inner_entry.key();
let (vec, _) = inner_entry.value();
let new_vec = vec.clone();
for x in vec.iter() {
let mut id = String::new();
write!(
@@ -159,9 +138,9 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
x.port,
x.is_http2,
x.to_https,
x.x4xx_limit.unwrap_or_default(),
x.rate_limit.unwrap_or_default(),
x.healthcheck.unwrap_or_default(),
x.x4xx_limit,
x.rate_limit,
x.healthcheck,
x.authorization
)
.unwrap_or(());
@@ -172,22 +151,8 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) {
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
let to_add = InnerMap {
address: Arc::from("127.0.0.1"),
port: 0,
is_ssl: false,
is_http2: false,
to_https: false,
rate_limit: None,
x4xx_limit: None,
healthcheck: None,
redirect_to: None,
authorization: None,
};
cloned.insert(id, Arc::from(to_add));
cloned.insert(hh, x.to_owned());
}
new_inner_map.insert(path.clone(), new_vec);
}
}
info!("Upstreams are fully populated. Ready to server requests");
@@ -223,33 +188,6 @@ pub fn listdir(dir: String) -> Vec<load::CertificateConfig> {
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>) {
thread::sleep(time::Duration::from_millis(10));
loop {
@@ -380,3 +318,49 @@ pub fn prepend(prefix: &str, val: &Option<Arc<str>>, uri: &str, port: &str) -> O
buf
})
}
pub fn write_pid_file(path: &str) -> std::io::Result<()> {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW) // refuse to follow symlinks
.open(path)?;
file.write_all(process::id().to_string().as_bytes())?;
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())
}
+27 -16
View File
@@ -1,32 +1,32 @@
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
pub type UpstreamsDashMap = DashMap<Arc<str>, DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>;
pub type UpstreamsIdMap = DashMap<String, Arc<InnerMap>>;
pub type Headers = DashMap<Arc<str>, DashMap<Arc<str>, Vec<(String, Arc<str>)>>>;
// pub type UpstreamsSerDde = Option<HashMap<String, HostConfig>>;
// pub type UpstreamsSerDe = HashMap<String, HostConfig>;
#[derive(Clone, Debug, Default)]
pub struct Extraparams {
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub sticky_sessions: u64,
pub rate_limit: isize,
pub x4xx_limit: u32,
pub to_https: bool,
pub ssl_verify: bool,
pub authentication: Option<Arc<InnerAuth>>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct GlobalServiceMapping {
pub redirect_to: Option<String>,
pub authorization: Option<String>,
pub upstream: String,
pub hostname: String,
pub path: Option<String>,
pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
@@ -36,14 +36,12 @@ pub struct GlobalServiceMapping {
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes {
pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul {
pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
@@ -112,6 +110,9 @@ pub struct AppConfig {
pub hc_method: String,
pub upstreams_conf: String,
pub log_level: String,
pub cache_size_mb: Option<usize>,
pub cache_ttl: Option<u64>,
pub access_log: Option<String>,
pub pid_file: Option<String>,
pub master_key: Option<String>,
pub config_address: String,
@@ -131,6 +132,9 @@ pub struct AppConfig {
pub runuser: Option<String>,
pub rungroup: 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_interval: Option<u64>,
pub tcp_keepalive_count: Option<usize>,
@@ -149,13 +153,20 @@ pub struct InnerMap {
pub is_ssl: bool,
pub is_http2: bool,
pub to_https: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>,
pub rate_limit: isize,
pub x4xx_limit: u32,
pub healthcheck: bool,
pub redirect_to: Option<Arc<str>>,
pub authorization: Option<Arc<InnerAuth>>,
}
#[allow(dead_code)]
impl Default for InnerMap {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl InnerMap {
pub fn new() -> Self {
@@ -181,9 +192,9 @@ pub struct InnerMapForJson {
pub is_ssl: bool,
pub is_http2: bool,
pub to_https: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub healthcheck: Option<bool>,
pub rate_limit: isize,
pub x4xx_limit: u32,
pub healthcheck: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct UpstreamSnapshotForJson {
+77
View File
@@ -0,0 +1,77 @@
use crate::tls::CertificateConfig;
use crate::utils::parceyaml::load_configuration;
use crate::utils::tools::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().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),
}
}
}
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(_) => {}
}
}
}