Compare commits

..
12 Commits
31 changed files with 2040 additions and 945 deletions
Generated
+694 -532
View File
File diff suppressed because it is too large Load Diff
+17 -10
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "aralez" name = "aralez"
version = "0.92.15" version = "0.93.2"
edition = "2021" edition = "2021"
license = "Apache-2.0" license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora" description = "Reverse proxy built on top of Cloudflare's Pingora"
@@ -15,7 +15,7 @@ panic = "abort"
strip = true strip = true
[dependencies] [dependencies]
tokio = { version = "1.52.3", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl pingora = { version = "0.8.1", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.8.1" pingora-core = "0.8.1"
pingora-proxy = "0.8.1" pingora-proxy = "0.8.1"
@@ -23,17 +23,15 @@ pingora-http = "0.8.1"
pingora-limits = "0.8.1" pingora-limits = "0.8.1"
pingora-cache = "0.8.1" pingora-cache = "0.8.1"
dashmap = "7.0.0-rc2" dashmap = "7.0.0-rc2"
async-trait = "0.1.89" async-trait = "0.1.91"
log = "0.4.33" log = "0.4.33"
notify = "9.0.0-rc.4" notify = "9.0.0-rc.4"
axum = { version = "0.8.9" } axum = { version = "0.8.9" }
reqwest = { version = "0.13.4", features = ["json", "stream", "blocking"] } serde = { version = "1.0.229", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] } noyalib = { version = "0.0.18", features = ["compat-serde-yaml"] }
noyalib = { version = "0.0.15", features = ["compat-serde-yaml"] }
rand = "0.10.2" rand = "0.10.2"
base64 = "0.22.1" base64 = "0.23.1"
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["use_pem", "rust_crypto"] } jsonwebtoken = { version = "11.0.0", default-features = false, features = ["use_pem", "rust_crypto"] }
tonic = "0.14.6"
sha2 = { version = "0.11.0-rc.5", default-features = false } sha2 = { version = "0.11.0-rc.5", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] } base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
@@ -43,7 +41,7 @@ x509-parser = "0.18.1"
rustls-pemfile = "2.2.0" rustls-pemfile = "2.2.0"
tower-http = { version = "0.7.0", features = ["fs"] } tower-http = { version = "0.7.0", features = ["fs"] }
privdrop = "0.5.6" privdrop = "0.5.6"
serde_json = "1.0.150" serde_json = "1.0.151"
subtle = "2.6.1" subtle = "2.6.1"
moka = { version = "0.12.15", features = ["sync"] } moka = { version = "0.12.15", features = ["sync"] }
ahash = "0.8.12" ahash = "0.8.12"
@@ -54,4 +52,13 @@ mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4" signal-hook = "0.4.4"
sd-notify = "0.5.0" sd-notify = "0.5.0"
libc = "0.2.186" libc = "0.2.186"
tokio-native-tls = "0.3.1"
hyper = "1.11.0"
hyper-util = "0.1.20"
http-body-util = "0.1.4"
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"
+26 -29
View File
@@ -6,13 +6,12 @@
### **Reverse proxy built on top of Cloudflare's Pingora** ### **Reverse proxy built on top of Cloudflare's Pingora**
Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a
featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a lightweight ingress-style proxy. lightweight ingress-style proxy.
--- ---
What Aralez means ? What Aralez means ? **Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
**Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box. Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
@@ -36,6 +35,7 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers
- **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge. - **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge.
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection. - **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **Built in rate limiter** — Globar or route limit requests to upstreams. - **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Caching Proxy** — In memory cache with configurable TTL, without external dependencies.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification. - **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth** - **Basic Auth**
- **API Key** via `x-api-key` header - **API Key** via `x-api-key` header
@@ -89,8 +89,7 @@ Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers
## Installation ## Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Make the binary executable `chmod 755 ./aralez-VERSION` and run.
Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names: File names:
@@ -108,8 +107,7 @@ File names:
**glibc** builds are in general faster, but have few, basic, Glibc dependencies: **glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies. **musl** builds are 100% portable, static compiled binaries and have zero system dependencies. In general musl builds have a little less performance.
In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones. The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
@@ -299,8 +297,7 @@ openssl req -x509 -newkey rsa:4096 \
## Remote Config API ## Remote Config API
Push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates. Push new `upstreams.yaml` over HTTP to `config_address` (`: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`
URL parameter. `key=MASTERKEY` is required. `MASTERKEY` is the value of `master_key` in the `main.yaml`
```bash ```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY} curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf?key=${MASTERKEY}
@@ -381,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** | | Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:| |--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot | | **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? | | **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes | | **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual | | **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | | **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual | | **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | | **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes | | **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes | | **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | | **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No | | **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes | | **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | Rust | C | C | Go | Go | C++ | | **Built With** | Rust | C | C | Go | Go | C++ |
--- ---
@@ -443,7 +440,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
## Results reflect synthetic performance under optimal conditions. ## Results reflect synthetic performance under optimal conditions.
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
- Duration : 10 Minutes - Duration : 10 Minutes
- Binary : aralez-x86_64-glibc - Binary : aralez-x86_64-glibc
@@ -499,7 +496,7 @@ Error distribution:
![Aralez](https://netangels.net/utils/glibc10.png) ![Aralez](https://netangels.net/utils/glibc10.png)
- CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections - 300 : simultaneous connections
- Duration : 10 Minutes - Duration : 10 Minutes
- Binary : aralez-x86_64-musl - Binary : aralez-x86_64-musl
@@ -557,7 +554,7 @@ Error distribution:
## Aralez, Nginx, Traefik performance benchmark ## Aralez, Nginx, Traefik performance benchmark
This benchmark is done on 4 servers. With CPU Intel(R) Xeon(R) E-2174G CPU @ 3.80GHz, 64 GB RAM. This benchmark is done on 4 servers. With CPU Intel (R) Xeon (R) E-2174G CPU @ 3.80GHz, 64 GB RAM.
1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could . 1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could .
2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance. 2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance.
+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"]
}
}
}
}
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
type: kubernetes.io/tls
data:
tls.crt: <base64-cert>
tls.key: <base64-key>
@@ -0,0 +1,43 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
image: sadoyan/aralez:latest
ports:
- containerPort: 80
- containerPort: 443
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
+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
@@ -0,0 +1,14 @@
# The file under watch and hot reload, changes are applied immediately, no need to restart or reload.
provider: "kubernetes" # "file" "consul" "kubernetes"
sticky_sessions: 172000
to_https: false
rate_limit: 500000
x4xx_limit: 100000
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "X-Global-Client:Yooooooo"
kubernetes:
servers:
- "127.0.0.1:6443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
@@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-nginx
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
+24
View File
@@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: default
annotations:
# These custom annotations are parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Some-Client:Some Custom Header", "X-Example-Client:An Example Header"]'
aralez.rs/server_headers: '["X-Some-Server:Some Custom Header", "X-Example-Server:An Example Header"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /ws
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Service
metadata:
name: svc-nginx
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+1 -4
View File
@@ -1,17 +1,14 @@
use crate::tls::acme::account::get_account; use crate::tls::acme::account::get_account;
use crate::utils::lazylock::CHALLENGES;
use crate::utils::parceyaml::DOMAINS; use crate::utils::parceyaml::DOMAINS;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy}; use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info}; use log::{error, info};
use pingora::prelude::sleep; use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair}; use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use std::collections::HashMap;
use std::fs; use std::fs;
use std::sync::{LazyLock, RwLock};
use std::time::Duration; use std::time::Duration;
use x509_parser::prelude::*; use x509_parser::prelude::*;
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
pub async fn refresh_order(certs_dir: String, autoconf_dir: String) { pub async fn refresh_order(certs_dir: String, autoconf_dir: String) {
let credsfile = autoconf_dir + "/acme_credentials.json"; let credsfile = autoconf_dir + "/acme_credentials.json";
loop { loop {
+5 -1
View File
@@ -1,15 +1,19 @@
pub mod auth; pub mod auth;
pub mod consul;
pub mod discovery; pub mod discovery;
mod filewatch; mod filewatch;
pub mod fordebug; pub mod fordebug;
pub mod grpc;
pub mod hcclient;
pub mod healthcheck; pub mod healthcheck;
pub mod httpclient; pub mod httpclient;
pub mod jwt; pub mod jwt;
pub mod kuberconsul; pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
pub mod lazylock; pub mod lazylock;
pub mod metrics; pub mod metrics;
pub mod parceyaml; pub mod parceyaml;
pub mod state; pub mod state;
pub mod structs; pub mod structs;
pub mod tools; pub mod tools;
// pub mod watchksecret;
+20 -19
View File
@@ -1,5 +1,6 @@
use crate::utils::jwt::{check_jwt, JWT_TOKEN}; use crate::utils::jwt::{check_jwt, JWT_TOKEN};
use crate::utils::structs::InnerAuth; use crate::utils::structs::InnerAuth;
pub(crate) use crate::utils::tools::split_host_port;
use axum::http::StatusCode; use axum::http::StatusCode;
use base64::engine::general_purpose::STANDARD; use base64::engine::general_purpose::STANDARD;
use base64::Engine; use base64::Engine;
@@ -224,22 +225,22 @@ pub fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string()) params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
} }
#[allow(clippy::needless_return)] // #[allow(clippy::needless_return)]
fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> { // pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') { // match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() { // Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)), // Ok(port) => return Some((h, port, tls, h)),
Err(_) => { // Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr); // log::warn!("ForwardAuth: invalid port in {}", addr);
return None; // return None;
} // }
}, // },
None => { // None => {
if tls { // if tls {
return Some((addr, 443u16, tls, addr)); // return Some((addr, 443u16, tls, addr));
} else { // } else {
return Some((addr, 80u16, tls, addr)); // return Some((addr, 80u16, tls, addr));
} // }
} // }
}; // };
} // }
+91
View File
@@ -0,0 +1,91 @@
use crate::utils::httpclient;
use crate::utils::kuberconsul::{list_to_upstreams, ServiceDiscovery};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{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) = crate::utils::kuberconsul::clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
sleep(Duration::from_secs(5)).await;
}
}
}
+3 -1
View File
@@ -1,5 +1,7 @@
use crate::utils::consul::ConsulDiscovery;
use crate::utils::filewatch; use crate::utils::filewatch;
use crate::utils::kuberconsul::{ConsulDiscovery, KubernetesDiscovery, ServiceDiscovery}; use crate::utils::kuberconsul::ServiceDiscovery;
use crate::utils::kubernetes::KubernetesDiscovery;
use crate::utils::structs::{Configuration, UpstreamsDashMap}; use crate::utils::structs::{Configuration, UpstreamsDashMap};
use crate::web::webserver; use crate::web::webserver;
use async_trait::async_trait; use async_trait::async_trait;
+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,
}
}
+95
View File
@@ -0,0 +1,95 @@
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 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)
}
+28 -78
View File
@@ -1,23 +1,21 @@
use crate::utils::hcclient::httpclient;
use crate::utils::lazylock::REVERSE_STORE; use crate::utils::lazylock::REVERSE_STORE;
use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap}; use crate::utils::structs::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::tools::*; use crate::utils::tools::*;
use dashmap::DashMap; use dashmap::DashMap;
use log::{error, warn};
use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::interval; use tokio::net::TcpStream;
use tonic::transport::Endpoint; use tokio::time::{interval, timeout};
use tokio_native_tls::native_tls::TlsConnector;
pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) { pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) {
let mut period = interval(Duration::from_secs(params.1)); let mut period = interval(Duration::from_secs(params.1));
let client = Client::builder().timeout(Duration::from_secs(params.1)).danger_accept_invalid_certs(true).build().unwrap();
loop { loop {
tokio::select! { tokio::select! {
_ = period.tick() => { _ = period.tick() => {
// populate_upstreams(&upslist, &fullist, &idlist, params, &client).await; let totest = build_upstreams(&fullist, params.0).await;
let totest = build_upstreams(&fullist, params.0, &client).await;
if !compare_dashmaps(&totest, &upslist) { if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist); clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist); clone_idmap_into(&totest, &idlist);
@@ -28,22 +26,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 { 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").await
build_upstreams(&fullist, "HEAD", &client).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 totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist); let fclone = clone_dashmap(fullist);
for val in fclone.iter() { for val in fclone.iter() {
@@ -56,18 +43,15 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
for upstream in path_entry.value().0.iter() { for upstream in path_entry.value().0.iter() {
let tls = if upstream.healthcheck.unwrap_or(true) { let tls = if upstream.healthcheck.unwrap_or(true) {
detect_tls(upstream.address.as_ref(), &upstream.port, client).await detect_tls(upstream.address.as_ref(), &upstream.port, host).await
} else { } else {
(false, None) false
}; };
let is_h2 = matches!(tls.1, Some(Version::HTTP_2));
let mut scheme = InnerMap { let mut scheme = InnerMap {
address: upstream.address.clone(), address: upstream.address.clone(),
port: upstream.port, port: upstream.port,
is_ssl: tls.0, is_ssl: tls,
is_http2: is_h2, is_http2: false,
to_https: upstream.to_https, to_https: upstream.to_https,
rate_limit: upstream.rate_limit, rate_limit: upstream.rate_limit,
x4xx_limit: upstream.x4xx_limit, x4xx_limit: upstream.x4xx_limit,
@@ -77,20 +61,17 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
}; };
if scheme.healthcheck.unwrap_or(true) { if scheme.healthcheck.unwrap_or(true) {
let link = if tls.0 { let link = if tls {
format!("https://{}:{}{}", upstream.address, upstream.port, path) format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else { } else {
format!("http://{}:{}{}", upstream.address, upstream.port, path) format!("http://{}:{}{}", upstream.address, upstream.port, path)
}; };
let resp = httpclient(method, tls, host, path, upstream.address.as_ref(), upstream.port, link).await;
let resp = http_request(&link, method, "", client).await;
if resp.0 { if resp.0 {
if resp.1 { if resp.1 {
scheme.is_http2 = is_h2; // could be adjusted further scheme.is_http2 = resp.1;
} }
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
} else {
warn!("Dead Upstream : {}", link);
} }
} else { } else {
innervec.push(Arc::from(scheme)); innervec.push(Arc::from(scheme));
@@ -103,51 +84,20 @@ async fn build_upstreams(fullist: &UpstreamsDashMap, method: &str, client: &Clie
totest totest
} }
async fn http_request(url: &str, method: &str, payload: &str, client: &Client) -> (bool, bool) { async fn detect_tls(ip: &str, port: &u16, sni: &str) -> bool {
if !["POST", "GET", "HEAD"].contains(&method) { let addr = format!("{ip}:{port}");
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method); let timeout_duration = Duration::from_secs(2); // THINK !!!
return (false, false); let stream = match timeout(timeout_duration, TcpStream::connect(&addr)).await {
} Ok(Ok(s)) => s,
async fn send_request(client: &Client, method: &str, url: &str, payload: &str) -> Option<reqwest::Response> { Ok(Err(_)) => return false,
match method {
"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)),
Err(_) => return false, Err(_) => return false,
}; };
tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await.ok().and_then(Result::ok).is_some() let connector = tokio_native_tls::TlsConnector::from(
} TlsConnector::builder()
.danger_accept_invalid_certs(true)
async fn detect_tls(ip: &str, port: &u16, client: &Client) -> (bool, Option<Version>) { .danger_accept_invalid_hostnames(true)
let https_url = format!("https://{}:{}", ip, port); .build()
if let Ok(response) = client.get(&https_url).send().await { .unwrap(),
return (true, Some(response.version())); );
} connector.connect(sni, stream).await.is_ok()
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)
}
}
}
} }
+218 -68
View File
@@ -1,85 +1,235 @@
use crate::utils::kuberconsul::{match_path, ConsulService, KubeEndpoints}; use crate::utils::consul::ConsulService;
use crate::utils::kuberconsul::match_path;
use crate::utils::kubernetes::KubeEndpoints;
use crate::utils::structs::{GlobalServiceMapping, InnerMap}; use crate::utils::structs::{GlobalServiceMapping, InnerMap};
use axum::http::{HeaderMap, HeaderValue}; use ahash::HashMap;
use dashmap::DashMap; use dashmap::DashMap;
use reqwest::Client; use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use serde::{Deserialize, Serialize};
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::{Arc, LazyLock};
use std::time::Duration; use std::time::Duration;
pub async fn for_consul(url: String, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub static CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().ok()?;
let mut headers = HeaderMap::new(); #[derive(Debug, Default, Serialize, Deserialize, Clone)]
if let Some(token) = token { pub struct ConsulServicesInternal {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap()); #[serde(rename = "aralez.host")]
} pub host: String,
let to = Duration::from_secs(1); #[serde(rename = "aralez.path")]
let resp = client.get(url).timeout(to).send().await.ok()?; pub path: String,
if !resp.status().is_success() { #[serde(rename = "aralez.auth")]
eprintln!("Consul API returned status: {}", resp.status()); pub auth: Option<String>,
#[serde(rename = "aralez.redirect")]
pub redirect: Option<String>,
#[serde(rename = "aralez.rate")]
pub rate: Option<isize>,
#[serde(rename = "aralez.4xx_rate")]
pub xrate: Option<u32>,
#[serde(rename = "aralez.client_headers")]
pub client_headers: Option<Vec<String>>,
#[serde(rename = "aralez.server_headers")]
pub server_headers: Option<Vec<String>>,
#[serde(rename = "aralez.to_https")]
pub to_https: Option<bool>,
}
pub type ConsulServices = HashMap<String, ConsulServicesInternal>;
pub async fn for_consul_list(url: &str, token: Option<String>) -> Option<ConsulServices> {
if let Some(data) = getfromapi(url, token, "consul").await {
let yo = parse_services(data);
if let Ok(y) = yo {
return Some(y);
}
return None; return None;
} }
let mut inner_vec = Vec::new(); None
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); }
let endpoints: Vec<ConsulService> = resp.json().await.ok()?;
for subsets in endpoints { fn parse_services(json: Vec<u8>) -> Result<ConsulServices, serde_json::Error> {
let addr = subsets.tagged_addresses.get("lan_ipv4").unwrap().address.clone(); let raw: HashMap<String, Vec<String>> = serde_json::from_slice(&json)?;
let prt = subsets.tagged_addresses.get("lan_ipv4").unwrap().port; Ok(raw
let to_add = Arc::from(InnerMap { .into_iter()
address: Arc::from(&*addr), .map(|(service, tags)| {
port: prt, let mut internal = ConsulServicesInternal::default();
is_ssl: false, for tag in tags {
is_http2: false, if let Some((key, value)) = tag.split_once('=') {
to_https: conf.to_https.unwrap_or(false), match key {
rate_limit: conf.rate_limit, "aralez.host" => internal.host = value.to_string(),
x4xx_limit: conf.x4xx_limit, "aralez.path" => internal.path = value.to_string(),
redirect_to: None, "aralez.rate" => internal.rate = value.parse::<isize>().ok(),
healthcheck: None, "aralez.4xx_rate" => internal.xrate = value.parse::<u32>().ok(),
authorization: None, "aralez.to_https" => internal.to_https = value.parse::<bool>().ok(),
}); "aralez.auth" => internal.auth = Option::from(value.to_string()),
inner_vec.push(to_add); "aralez.redirect" => internal.redirect = Option::from(value.to_string()),
} "aralez.client_header" => internal.client_headers.get_or_insert_default().push(value.to_string()),
match_path(conf, &upstreams, inner_vec); "aralez.server_header" => internal.server_headers.get_or_insert_default().push(value.to_string()),
Some(upstreams) _ => {}
}
}
}
(service, internal)
})
.collect())
}
pub async fn for_consul(url: &str, token: Option<String>, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
if let Some(data) = getfromapi(url, token, "consul").await {
let endpoints: Vec<ConsulService> = serde_json::from_slice(&data).ok()?;
let mut inner_vec = Vec::new();
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
for subsets in endpoints {
let to_add = Arc::from(InnerMap {
address: Arc::from(&*subsets.address),
port: subsets.port,
is_ssl: false,
is_http2: false,
to_https: conf.to_https.unwrap_or(false),
rate_limit: conf.rate_limit,
x4xx_limit: conf.x4xx_limit,
redirect_to: conf.redirect_to.clone().map(Arc::<str>::from),
healthcheck: None,
authorization: None,
});
inner_vec.push(to_add);
}
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
};
None
} }
pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> { pub async fn for_kuber(url: &str, token: &str, conf: &GlobalServiceMapping) -> Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>> {
let to = Duration::from_secs(10); if let Some(data) = getfromapi(url, Some(token.to_string()), "kubernetes").await {
let client = Client::builder().timeout(Duration::from_secs(10)).danger_accept_invalid_certs(true).build().ok()?; let endpoints: KubeEndpoints = serde_json::from_slice(&data).ok()?;
let resp = client.get(url).timeout(to).bearer_auth(token).send().await.ok()?; let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new();
if !resp.status().is_success() { let mut inner_vec = Vec::new();
eprintln!("Kubernetes API returned status: {}", resp.status());
return None;
}
let endpoints: KubeEndpoints = resp.json().await.ok()?;
let upstreams: DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)> = DashMap::new(); if let Some(subsets) = endpoints.subsets {
for subset in subsets {
if let Some(subsets) = endpoints.subsets { if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) {
for subset in subsets { for addr in addresses {
if let (Some(addresses), Some(ports)) = (subset.addresses, subset.ports) { for port in &ports {
let mut inner_vec = Vec::new(); let to_add = Arc::from(InnerMap {
for addr in addresses { address: Arc::from(addr.ip.as_str()),
for port in &ports { port: port.port,
// let redirect_link = conf.redirect_to.as_ref().map(|www| Arc::from(www.as_str())); is_ssl: false,
let to_add = Arc::from(InnerMap { is_http2: false,
address: Arc::from(addr.ip.clone()), to_https: conf.to_https.unwrap_or(false),
port: port.port, rate_limit: conf.rate_limit,
is_ssl: false, x4xx_limit: conf.x4xx_limit,
is_http2: false, healthcheck: None,
to_https: conf.to_https.unwrap_or(false), redirect_to: None,
rate_limit: conf.rate_limit, authorization: None,
x4xx_limit: conf.x4xx_limit, });
healthcheck: None, inner_vec.push(to_add);
redirect_to: None, }
authorization: None,
});
inner_vec.push(to_add);
} }
} }
match_path(conf, &upstreams, inner_vec.clone());
} }
} }
if !inner_vec.is_empty() {
match_path(conf, &upstreams, inner_vec);
return Some(upstreams);
}
} }
Some(upstreams) None
} }
pub async fn getfromapi(url: &str, token: Option<String>, provider: &str) -> Option<Vec<u8>> {
let (host, port, path, is_tls) = parse_url(&url).ok()?;
let mut peer = HttpPeer::new((host, port), is_tls, host.to_string());
peer.options.total_connection_timeout = Some(Duration::from_secs(5));
peer.options.read_timeout = Some(Duration::from_secs(5));
if is_tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
peer.options.alpn = ALPN::H2H1;
}
let mut http_session = CONNECTOR.get_http_session(&peer).await.ok()?;
let mut req = RequestHeader::build("GET", path.as_bytes(), None).ok()?;
let host_header = if (is_tls && port == 443) || (!is_tls && port == 80) {
host.to_string()
} else {
format!("{}:{}", host, port)
};
req.insert_header("Host", host_header).ok()?;
req.insert_header("Accept", "application/json").ok()?;
match provider {
"consul" => {
if let Some(token) = token {
req.insert_header("X-Consul-Token", token).ok()?;
}
}
"kubernetes" => {
if let Some(token) = token {
req.insert_header("Authorization", format!("Bearer {}", token)).ok()?;
}
}
_ => {}
}
if http_session.0.write_request_header(Box::new(req)).await.is_err() {
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
return None;
}
let status = match http_session.0.read_response_header().await {
Ok(_) => http_session.0.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("API call failed to read response header for {}: {}", url, e);
500
}
};
let mut body_bytes = Vec::new();
if status == 200 {
while let Ok(Some(chunk)) = http_session.0.read_response_body().await {
body_bytes.extend_from_slice(&chunk);
}
}
CONNECTOR.release_http_session(http_session.0, &peer, None).await;
if status == 200 && !body_bytes.is_empty() {
Some(body_bytes)
} else {
None
}
}
fn parse_url(url: &str) -> Result<(&str, u16, &str, bool), &'static str> {
let is_https = url.starts_with("https://");
let default_port = if is_https { 443 } else { 80 };
let no_scheme = url.split_once("://").map_or(url, |(_, rest)| rest);
let (authority, uri) = no_scheme.find('/').map_or((no_scheme, "/"), |i| (&no_scheme[..i], &no_scheme[i..]));
let (host, port) = match authority.split_once(':') {
Some((h, p)) => {
let port_num = p.parse::<u16>().map_err(|_| "Invalid port number")?;
(h, port_num)
}
None => (authority, default_port),
};
if host.is_empty() {
return Err("Empty host in URL");
}
Ok((host, port, uri, is_https))
}
// fn base64_decode(encoded: &str) -> Option<String> {
// let decoded_bytes = general_purpose::STANDARD.decode(encoded).unwrap_or_default();
// let decoded: Result<HashMap<String, String>, serde_json::Error> = serde_json::from_slice(&decoded_bytes);
// println!("Decoded: {:?}", decoded);
// None
// }
+14 -179
View File
@@ -1,216 +1,50 @@
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap}; use crate::utils::structs::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams}; use crate::utils::tools::{clone_dashmap_into, compare_dashmaps, print_upstreams};
use async_trait::async_trait; use async_trait::async_trait;
use dashmap::DashMap; use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tokio::fs::File; use tokio::fs::File;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
pub struct KubeEndpoints {
pub subsets: Option<Vec<KubeSubset>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeSubset {
pub addresses: Option<Vec<KubeAddress>>,
pub ports: Option<Vec<KubePort>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeAddress {
pub ip: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubePort {
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "ServiceTaggedAddresses")]
pub tagged_addresses: HashMap<String, ConsulTaggedAddress>,
}
#[derive(Debug, Deserialize)]
pub struct ConsulTaggedAddress {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "Port")]
pub port: u16,
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) { pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
if let Some(list) = lt { if let Some(list) = lt {
match upstreams.get(&*i.hostname.clone()) { let key: Arc<str> = Arc::from(i.hostname.as_str());
match upstreams.get(&key) {
Some(upstr) => { Some(upstr) => {
for (k, v) in list { for (k, v) in list {
upstr.value().insert(k.to_owned(), v); upstr.value().insert(k, v);
} }
} }
None => { None => {
upstreams.insert(Arc::from(i.hostname.clone()), list); upstreams.insert(key, list);
} }
}; };
} }
} }
pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) { pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
match conf.path { let path = conf.path.as_deref().unwrap_or("/");
Some(ref p) => { upstreams.insert(Arc::from(path), (values, AtomicUsize::new(0)));
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 { pub async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.unwrap(); let mut file = File::open(path).await.unwrap();
let mut contents = String::new(); let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap(); file.read_to_string(&mut contents).await.unwrap();
contents.trim().to_string() contents.trim().to_string()
} }
#[async_trait] #[async_trait]
pub trait ServiceDiscovery { pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>); async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
} }
pub struct KubernetesDiscovery; pub async fn clone_compare(upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
pub struct ConsulDiscovery; if !compare_dashmaps(upstreams, &config.upstreams) {
let tosend = Configuration {
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).unwrap().to_string();
let path = kuber.tokenpath.unwrap_or("/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let namespace = get_current_namespace().unwrap_or_else(|| "default".to_string());
let token = read_token(path.as_str()).await;
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(kuber) = config.kubernetes.clone() {
if let Some(svc) = kuber.services {
for service in svc {
let header_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut hl = Vec::new();
build_headers(&service.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match service.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(Arc::from(path.as_str()), hl);
// header_list.insert(Arc::from(i.path).unwrap_or(Arc::from("/")).as_str(), hl);
config.client_headers.insert(Arc::from(service.hostname.clone()), header_list);
}
let url = format!("https://{}/api/v1/namespaces/{}/endpoints/{}", server, namespace, service.hostname);
// let url = format!("https://{}/api/v1/namespaces/{}/endpoints?labelSelector=app", server, namespace);
let list = httpclient::for_kuber(&url, &token, &service).await;
// println!("{:?}", list);
list_to_upstreams(list, &upstreams, &service);
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
}
sleep(Duration::from_secs(5)).await;
}
}
}
}
fn get_current_namespace() -> Option<String> {
let ns_path = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";
if Path::new(ns_path).exists() {
if let Ok(contents) = fs::read_to_string(ns_path) {
return Some(contents.trim().to_string());
}
}
std::env::var("POD_NAMESPACE").ok()
}
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
let prev_upstreams = UpstreamsDashMap::new();
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or(vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or("0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or("0".to_string())
)]);
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let consul_data = servers.get(num).unwrap().to_string();
let ss = consul_data + "/v1/catalog/service/";
if let Some(svc) = consul.services {
for i in svc {
let header_list = DashMap::new();
let mut hl = Vec::new();
build_headers(&i.client_headers, config.as_ref(), &mut hl);
if !hl.is_empty() {
match i.path.clone() {
Some(path) => {
header_list.insert(Arc::from(path.as_str()), hl);
}
None => {
header_list.insert(Arc::from("/"), hl);
}
}
// header_list.insert(i.path.clone().unwrap_or("/".to_string()), hl);
config.client_headers.insert(Arc::from(i.hostname.clone()), header_list);
}
let pref = ss.clone() + &i.upstream;
let list = httpclient::for_consul(pref, consul.token.clone(), &i).await;
list_to_upstreams(list, &upstreams, &i);
}
}
}
if let Some(lt) = clone_compare(&upstreams, &prev_upstreams, &config).await {
toreturn.send(lt).await.unwrap();
}
sleep(Duration::from_secs(5)).await;
}
}
}
async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(upstreams, prev_upstreams) {
let tosend: Configuration = Configuration {
upstreams: Default::default(), upstreams: Default::default(),
client_headers: config.client_headers.clone(), client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(), server_headers: config.server_headers.clone(),
@@ -219,10 +53,11 @@ async fn clone_compare(upstreams: &UpstreamsDashMap, prev_upstreams: &UpstreamsD
typecfg: config.typecfg.clone(), typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(), extraparams: config.extraparams.clone(),
}; };
clone_dashmap_into(upstreams, prev_upstreams); clone_dashmap_into(upstreams, &config.upstreams);
clone_dashmap_into(upstreams, &tosend.upstreams); clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams); print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend); return Some(tosend);
}; }
None None
} }
+157
View File
@@ -0,0 +1,157 @@
use crate::utils::httpclient;
use crate::utils::kubewatcher::{start_ingress_watcher_with_config, AralezRouteUpdate};
use crate::utils::parceyaml::build_headers;
use crate::utils::structs::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use log::error;
use rand::RngExt;
use std::env;
use std::fs;
use std::path::Path;
use std::sync::Arc;
use crate::utils::kuberconsul::{clone_compare, list_to_upstreams, ServiceDiscovery};
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
pub struct KubeEndpoints {
pub subsets: Option<Vec<KubeSubset>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeSubset {
pub addresses: Option<Vec<KubeAddress>>,
pub ports: Option<Vec<KubePort>>,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubeAddress {
pub ip: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct KubePort {
pub port: u16,
}
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 namespace = get_current_namespace().unwrap_or_else(|| "default".to_string());
let token = crate::utils::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,
// sticky_sessions: update.sticky_sessions,
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://{}/api/v1/namespaces/{}/endpoints/{}", server, 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;
}
}
}
}
}
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());
}
}
env::var("POD_NAMESPACE").ok()
}
+271
View File
@@ -0,0 +1,271 @@
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::time::Duration;
use tokio::sync::mpsc;
pub struct AralezRouteUpdate {
pub host: String,
pub path: String,
pub service_name: String,
pub is_deleted: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub sticky_sessions: Option<u64>,
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>,
sticky_sessions: Option<u64>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
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 svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let external_ip = match discover_controller_ip(&client, ns, &svc_name).await {
Some(ip) => ip,
None => discover_node_ip(&client)
.await
.unwrap_or_else(|| std::env::var("ARALEZ_EXTERNAL_IP").unwrap_or_else(|_| "127.0.0.1".to_string())),
};
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let status = ing.status.clone();
tokio::spawn(async move {
patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, status.as_ref(), &external_ip).await;
});
}
}
if let Some(rules) = &spec.rules {
for rule in rules {
let host = match &rule.host {
Some(h) => h.clone(),
None => "*".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 mut update = AralezRouteUpdate {
host: host.clone(),
path,
service_name,
is_deleted,
rate_limit: None,
x4xx_limit: None,
sticky_sessions: None,
client_headers: None,
server_headers: None,
};
if let Some(annot) = parse_ingress_config(ing) {
update.rate_limit = annot.rate_limit;
update.x4xx_limit = annot.x4xx_limit;
update.sticky_sessions = annot.sticky_sessions;
update.client_headers = annot.client_headers;
update.server_headers = annot.server_headers;
}
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) -> Option<AralezAnnotation> {
if let Some(annotations) = &ing.metadata.annotations {
let mut clh: Option<Vec<String>> = None;
let mut srh: Option<Vec<String>> = None;
if let Some(ch) = annotations.get("aralez.rs/client_headers") {
let data: Vec<String> = serde_json::from_str(ch).unwrap_or_default();
clh = Some(data);
}
if let Some(sh) = annotations.get("aralez.rs/server_headers") {
let data: Vec<String> = serde_json::from_str(sh).unwrap_or_default();
srh = Some(data);
};
let annots = AralezAnnotation {
rate_limit: annotations.get("aralez.rs/rate_limit").and_then(|v| v.parse().ok()),
x4xx_limit: annotations.get("aralez.rs/x4xx_limit").and_then(|v| v.parse().ok()),
sticky_sessions: annotations.get("aralez.rs/sticky_sessions").and_then(|v| v.parse().ok()),
client_headers: clh,
server_headers: srh,
};
return Some(annots);
}
None
}
pub async fn patch_ingress_status_address(
client: &Client,
namespace: &str,
name: &str,
current_status: Option<&k8s_openapi::api::networking::v1::IngressStatus>,
external_ip: &str,
) {
if let Some(status) = current_status {
if let Some(lb) = &status.load_balancer {
if let Some(ing_list) = &lb.ingress {
if ing_list.iter().any(|i| i.ip.as_deref() == Some(external_ip)) {
return;
}
}
}
}
let ingresses: Api<Ingress> = Api::namespaced(client.clone(), namespace);
let patch = json!({
"apiVersion": "networking.k8s.io/v1",
"kind": "Ingress",
"status": {
"loadBalancer": {
"ingress": [
{
"ip": external_ip
}
]
}
}
});
let params = PatchParams::apply("aralez-ingress-controller").force();
match ingresses.patch_status(name, &params, &Patch::Apply(patch)).await {
Ok(_) => info!("Patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip),
Err(e) => error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, 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
}
+3 -1
View File
@@ -4,9 +4,10 @@ use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::lock::CacheLock; use pingora_cache::lock::CacheLock;
use pingora_cache::MemCache; use pingora_cache::MemCache;
use pingora_limits::rate::Rate; use pingora_limits::rate::Rate;
use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use std::sync::OnceLock;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use std::sync::{OnceLock, RwLock};
use std::time::Duration; use std::time::Duration;
pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new); pub static REVERSE_STORE: LazyLock<DashMap<String, String>> = LazyLock::new(DashMap::new);
@@ -17,3 +18,4 @@ pub static MEM_CACHE: LazyLock<MemCache> = LazyLock::new(MemCache::new);
pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30))); pub static CACHE_LOCK: LazyLock<CacheLock> = LazyLock::new(|| CacheLock::new(Duration::from_secs(30)));
pub static EVICTION: OnceLock<Manager> = OnceLock::new(); pub static EVICTION: OnceLock<Manager> = OnceLock::new();
pub static CACHE_TTL: OnceLock<u64> = OnceLock::new(); pub static CACHE_TTL: OnceLock<u64> = OnceLock::new();
pub static CHALLENGES: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
-11
View File
@@ -16,14 +16,3 @@ pub fn is_first_run() -> bool {
let state = GLOBAL_STATE.read().unwrap(); let state = GLOBAL_STATE.read().unwrap();
state.first_run state.first_run
} }
/*
impl SharedState {
pub fn mark_first_run(&mut self) {
self.first_run = false;
}
pub fn is_first_run(&self) -> bool {
self.first_run
}
}
*/
+2 -3
View File
@@ -20,11 +20,12 @@ pub struct Extraparams {
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct GlobalServiceMapping { pub struct GlobalServiceMapping {
pub redirect_to: Option<String>,
pub authorization: Option<String>,
pub upstream: String, pub upstream: String,
pub hostname: String, pub hostname: String,
pub path: Option<String>, pub path: Option<String>,
pub to_https: Option<bool>, pub to_https: Option<bool>,
pub sticky_sessions: Option<u64>,
pub rate_limit: Option<isize>, pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>, pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>, pub client_headers: Option<Vec<String>>,
@@ -34,14 +35,12 @@ pub struct GlobalServiceMapping {
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Kubernetes { pub struct Kubernetes {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub tokenpath: Option<String>, pub tokenpath: Option<String>,
} }
#[derive(Clone, Default, Debug, Serialize, Deserialize)] #[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct Consul { pub struct Consul {
pub servers: Option<Vec<String>>, pub servers: Option<Vec<String>>,
pub services: Option<Vec<GlobalServiceMapping>>,
pub token: Option<String>, pub token: Option<String>,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
+20
View File
@@ -377,3 +377,23 @@ pub fn write_pid_file(path: &str) -> std::io::Result<()> {
file.write_all(process::id().to_string().as_bytes())?; file.write_all(process::id().to_string().as_bytes())?;
Ok(()) Ok(())
} }
#[allow(clippy::needless_return)]
pub fn split_host_port(addr: &str, tls: bool) -> Option<(&str, u16, bool, &str)> {
match addr.split_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => return Some((h, port, tls, h)),
Err(_) => {
log::warn!("ForwardAuth: invalid port in {}", addr);
return None;
}
},
None => {
if tls {
return Some((addr, 443u16, tls, addr));
} else {
return Some((addr, 80u16, tls, addr));
}
}
};
}
+1 -5
View File
@@ -1,5 +1,5 @@
use crate::tls::acme::order::CHALLENGES;
use crate::tls::acme::{account, order}; use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body; use axum::body::Body;
use axum::extract::State; use axum::extract::State;
use axum::http::{Response, StatusCode}; use axum::http::{Response, StatusCode};
@@ -44,10 +44,6 @@ pub async fn acme_order(State(state): State<crate::web::webserver::AppState>, ax
} }
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse { pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() { if let Ok(challenges) = CHALLENGES.read() {
// for k in challenges.iter() {
// println!(" ==> {} : {}", k.0, k.1);
// }
if let Some(key_authorization) = challenges.get(&token) { if let Some(key_authorization) = challenges.get(&token) {
return Response::builder() return Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
+5 -4
View File
@@ -190,7 +190,7 @@ impl ProxyHttp for LB {
Some(hostname) => match ctx.upstream_peer.as_ref() { Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => { Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string())); 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; peer.options.alpn = ALPN::H2;
} }
if innermap.is_ssl { if innermap.is_ssl {
@@ -310,15 +310,16 @@ impl ProxyHttp for LB {
let _ = _upstream_response.append_header("set-cookie", buf.as_str()); let _ = _upstream_response.append_header("set-cookie", buf.as_str());
} }
} }
Ok(())
}
async fn upstream_response_filter(&self, _session: &mut Session, upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(client_headers) = &ctx.client_headers { if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() { for (k, v) in client_headers.iter() {
_upstream_response.append_header(k.clone(), v.as_ref())?; upstream_response.append_header(k.clone(), v.as_ref())?;
} }
} }
Ok(()) Ok(())
} }
async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) { async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) {
let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16()); let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
let m = &MetricTypes { let m = &MetricTypes {