From 4126249bcd90664e9b2a7da6fcd4b2729d4c7772 Mon Sep 17 00:00:00 2001 From: Ara Sadoyan Date: Mon, 16 Jun 2025 13:29:13 +0200 Subject: [PATCH] Project rename. Load multiple certificates from folder. --- Cargo.lock | 179 +++++++++++++++++++++++++++++++-------- Cargo.toml | 9 +- METRICS.md | 46 +++++----- README.md | 137 +++++++++++++++--------------- etc/main.yaml | 13 ++- etc/stresstest.yaml | 2 +- src/utils.rs | 1 + src/utils/auth.rs | 2 +- src/utils/healthcheck.rs | 2 +- src/utils/metrics.rs | 16 ++-- src/utils/structs.rs | 5 +- src/utils/tls.rs | 176 ++++++++++++++++++++++++++++++++++++++ src/utils/tools.rs | 34 +++++++- src/web/proxyhttp.rs | 34 +++++--- src/web/start.rs | 19 ++++- src/web/webserver.rs | 20 ++--- 16 files changed, 524 insertions(+), 171 deletions(-) create mode 100644 src/utils/tls.rs diff --git a/Cargo.lock b/Cargo.lock index d7de5a4..a45854e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,42 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "aralez" +version = "0.9.1" +dependencies = [ + "arc-swap", + "async-trait", + "axum", + "axum-server", + "base16ct", + "base64", + "dashmap", + "env_logger", + "futures", + "jsonwebtoken", + "lazy_static", + "log", + "mimalloc", + "notify", + "openssl", + "pingora", + "pingora-core", + "pingora-http", + "pingora-proxy", + "prometheus 0.14.0", + "rand 0.9.1", + "reqwest", + "rustls-pemfile", + "serde", + "serde_yaml 0.9.34+deprecated", + "sha2", + "tokio", + "tonic", + "urlencoding", + "x509-parser", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -122,6 +158,45 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -556,6 +631,26 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.4.0" @@ -835,39 +930,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gazan" -version = "0.1.0" -dependencies = [ - "arc-swap", - "async-trait", - "axum", - "axum-server", - "base16ct", - "base64", - "dashmap", - "env_logger", - "futures", - "jsonwebtoken", - "lazy_static", - "log", - "mimalloc", - "notify", - "pingora", - "pingora-core", - "pingora-http", - "pingora-proxy", - "prometheus 0.14.0", - "rand 0.9.1", - "reqwest", - "serde", - "serde_yaml 0.9.34+deprecated", - "sha2", - "tokio", - "tonic", - "urlencoding", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1539,6 +1601,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -1614,6 +1682,16 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.0.0" @@ -1682,6 +1760,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -2410,6 +2497,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.0.7" @@ -3611,6 +3707,23 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.12", + "time", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 6184a97..45f2dbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "gazan" -version = "0.1.0" +name = "aralez" +version = "0.9.1" edition = "2021" [profile.release] @@ -43,4 +43,9 @@ arc-swap = "1.7.1" mimalloc = { version = "0.1.46", default-features = false } prometheus = "0.14.0" lazy_static = "1.5.0" +openssl = "0.10.72" +x509-parser = "0.17.0" +rustls-pemfile = "2.2.0" + +#openssl = "0.10.72" diff --git a/METRICS.md b/METRICS.md index c6b11a5..72d4579 100644 --- a/METRICS.md +++ b/METRICS.md @@ -1,6 +1,6 @@ -# ๐Ÿ“ˆ Gazan Prometheus Metrics Reference +# ๐Ÿ“ˆ Aralez Prometheus Metrics Reference -This document outlines Prometheus metrics for the [Gazan](https://github.com/sadoyan/gazan) reverse proxy. +This document outlines Prometheus metrics for the [Aralez](https://github.com/sadoyan/aralez) reverse proxy. These metrics can be used for monitoring, alerting and performance analysis. Exposed to `http://config_address/metrics` @@ -9,26 +9,26 @@ By default `http://127.0.0.1:3000/metrics` # ๐Ÿ“Š Example Grafana dashboard during stress test : -![Gazan](https://netangels.net/utils/dash.png) +![Aralez](https://netangels.net/utils/dash.png) --- ## ๐Ÿ› ๏ธ Prometheus Metrics -### 1. `gazan_requests_total` +### 1. `aralez_requests_total` - **Type**: `Counter` -- **Purpose**: Total amount requests served by Gazan. +- **Purpose**: Total amount requests served by Aralez. **PromQL example:** ```promql -rate(gazan_requests_total[5m]) +rate(aralez_requests_total[5m]) ``` --- -### 2. `gazan_errors_total` +### 2. `aralez_errors_total` - **Type**: `Counter` - **Purpose**: Count of requests that resulted in an error. @@ -36,12 +36,12 @@ rate(gazan_requests_total[5m]) **PromQL example:** ```promql -rate(gazan_errors_total[5m]) +rate(aralez_errors_total[5m]) ``` --- -### 3. `gazan_responses_total{status="200"}` +### 3. `aralez_responses_total{status="200"}` - **Type**: `CounterVec` - **Purpose**: Count of responses by HTTP status code. @@ -49,14 +49,14 @@ rate(gazan_errors_total[5m]) **PromQL example:** ```promql -rate(gazan_responses_total{status=~"5.."}[5m]) > 0 +rate(aralez_responses_total{status=~"5.."}[5m]) > 0 ``` > Useful for alerting on 5xx errors. --- -### 4. `gazan_response_latency_seconds` +### 4. `aralez_response_latency_seconds` - **Type**: `Histogram` - **Purpose**: Tracks the latency of responses in seconds. @@ -64,13 +64,13 @@ rate(gazan_responses_total{status=~"5.."}[5m]) > 0 **Example bucket output:** ```prometheus -gazan_response_latency_seconds_bucket{le="0.01"} 15 -gazan_response_latency_seconds_bucket{le="0.1"} 120 -gazan_response_latency_seconds_bucket{le="0.25"} 245 -gazan_response_latency_seconds_bucket{le="0.5"} 500 +aralez_response_latency_seconds_bucket{le="0.01"} 15 +aralez_response_latency_seconds_bucket{le="0.1"} 120 +aralez_response_latency_seconds_bucket{le="0.25"} 245 +aralez_response_latency_seconds_bucket{le="0.5"} 500 ... -gazan_response_latency_seconds_count 1023 -gazan_response_latency_seconds_sum 42.6 +aralez_response_latency_seconds_count 1023 +aralez_response_latency_seconds_sum 42.6 ``` | Metric | Meaning | @@ -91,14 +91,14 @@ gazan_response_latency_seconds_sum 42.6 ๐Ÿ”น **95th percentile latency** ```promql -histogram_quantile(0.95, rate(gazan_response_latency_seconds_bucket[5m])) +histogram_quantile(0.95, rate(aralez_response_latency_seconds_bucket[5m])) ``` ๐Ÿ”น **Average latency** ```promql -rate(gazan_response_latency_seconds_sum[5m]) / rate(gazan_response_latency_seconds_count[5m]) +rate(aralez_response_latency_seconds_sum[5m]) / rate(aralez_response_latency_seconds_count[5m]) ``` --- @@ -112,9 +112,9 @@ rate(gazan_response_latency_seconds_sum[5m]) / rate(gazan_response_latency_secon | Metric Name | Type | What it Tells You | |---------------------------------------|------------|---------------------------| -| `gazan_requests_total` | Counter | Total requests served | -| `gazan_errors_total` | Counter | Number of failed requests | -| `gazan_responses_total{status="200"}` | CounterVec | Response status breakdown | -| `gazan_response_latency_seconds` | Histogram | How fast responses are | +| `aralez_requests_total` | Counter | Total requests served | +| `aralez_errors_total` | Counter | Number of failed requests | +| `aralez_responses_total{status="200"}` | CounterVec | Response status breakdown | +| `aralez_response_latency_seconds` | Histogram | How fast responses are | ๐Ÿ“˜ *Last updated: May 2025* diff --git a/README.md b/README.md index d6495d9..866f6a7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -![Gazan](https://netangels.net/utils/gazan-white.jpg) +![Aralez](https://netangels.net/utils/aralez-white.jpg) -# Gazan - The beast-mode reverse proxy. +# Aralez - The beast-mode reverse proxy. -Gazan is a Reverse proxy, service mesh based on Cloudflare's Pingora +Aralez is a Reverse proxy, service mesh based on Cloudflare's Pingora -**What Gazan means?** -Gazan = ิณีกีฆีกีถ = beast / wild animal in Armenian / Often used as a synonym to something great.. +**What Aralez means?** +Aralez = ิณีกีฆีกีถ = beast / wild animal in Armenian / Often used as a synonym to something great.. -Built on Rust, on top of **Cloudflareโ€™s Pingora engine**, **Gazan** 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. --- @@ -15,11 +15,11 @@ Built on Rust, on top of **Cloudflareโ€™s Pingora engine**, **Gazan** delivers w - **Dynamic Config Reloads** โ€” Upstreams can be updated live via API, no restart required. - **TLS Termination** โ€” Built-in OpenSSL support. -- **Upstreams TLS detection** โ€” Gazan will automatically detect if upstreams uses secure connection. +- **Upstreams TLS detection** โ€” Aralez will automatically detect if upstreams uses secure connection. - **Authentication** โ€” Supports Basic Auth, API tokens, and JWT verification. - **Basic Auth** - **API Key** via `x-api-key` header - - **JWT Auth**, with tokens issued by Gazan itself via `/jwt` API + - **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API - โฌ‡๏ธ See below for examples and implementation details. - **Load Balancing Strategies** - Round-robin @@ -63,29 +63,28 @@ Built on Rust, on top of **Cloudflareโ€™s Pingora engine**, **Gazan** delivers w ### ๐Ÿ”ง `main.yaml` -| Key | Example Value | Description | -|----------------------------------|--------------------------------------|-------------------------------------------------------------------------------------------------| -| **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 | -| **user** | gazan | Optional, Username for running gazan after dropping root privileges, requires to launch as root | -| **group** | gazan | Optional,Group for running gazan after dropping root privileges, requires to launch as root | -| **daemon** | false | Run in background (boolean) | -| **upstream_keepalive_pool_size** | 500 | Pool size for upstream keepalive connections | -| **pid_file** | /tmp/gazan.pid | Path to PID file | -| **error_log** | /tmp/gazan_err.log | Path to error log file | -| **upgrade_sock** | /tmp/gazan.sock | Path to live upgrade socket file | -| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | -| **config_tls_address** | 0.0.0.0:3001 | HTTPS API address for pushing upstreams.yaml from remote location | -| **config_tls_certificate** | etc/server.crt | Certificate file path for API. Mandatory if proxy_address_tls is set, else optional | -| **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional | -| **proxy_address_http** | 0.0.0.0:6193 | Gazan HTTP bind address | -| **proxy_address_tls** | 0.0.0.0:6194 | Gazan HTTPS bind address (Optional) | -| **tls_certificate** | etc/server.crt | TLS certificate file path. Mandatory if proxy_address_tls is set, else optional | -| **tls_key_file** | etc/key.pem | TLS Key file path. Mandatory if proxy_address_tls is set, else optional | -| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file | -| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off | -| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE | -| **hc_interval** | 2 | Interval for health checks in seconds | -| **master_key** | 5aeff7f9-7b94-447c-af60-e8c488544a3e | Master key for working with API server and JWT Secret generation | +| Key | Example Value | Description | +|----------------------------------|--------------------------------------|--------------------------------------------------------------------------------------------------| +| **threads** | 12 | Number of running daemon threads. Optional, defaults to 1 | +| **user** | aralez | Optional, Username for running aralez after dropping root privileges, requires to launch as root | +| **group** | aralez | Optional,Group for running aralez after dropping root privileges, requires to 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 | +| **error_log** | /tmp/aralez_err.log | Path to error log file | +| **upgrade_sock** | /tmp/aralez.sock | Path to live upgrade socket file | +| **config_address** | 0.0.0.0:3000 | HTTP API address for pushing upstreams.yaml from remote location | +| **config_tls_address** | 0.0.0.0:3001 | HTTPS API address for pushing upstreams.yaml from remote location | +| **config_tls_certificate** | etc/server.crt | Certificate file path for API. Mandatory if proxy_address_tls is set, else optional | +| **config_tls_key_file** | etc/key.pem | Private Key file path. Mandatory if proxy_address_tls is set, else optional | +| **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_certificates** | etc/certs/ | The directory containing certificate and key files. In a format {NAME}.crt, {NAME}.key. | +| **upstreams_conf** | etc/upstreams.yaml | The location of upstreams file | +| **log_level** | info | Log level , possible values : info, warn, error, debug, trace, off | +| **hc_method** | HEAD | Healthcheck method (HEAD, GET, POST are supported) UPPERCASE | +| **hc_interval** | 2 | Interval for health checks in seconds | +| **master_key** | 5aeff7f9-7b94-447c-af60-e8c488544a3e | Master key for working with API server and JWT Secret generation | ### ๐ŸŒ `upstreams.yaml` @@ -101,40 +100,40 @@ Built on Rust, on top of **Cloudflareโ€™s Pingora engine**, **Gazan** delivers w ## ๐Ÿ›  Installation -Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/gazan/releases) repo -Make the binary executable `chmod 755 ./gazan-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: -| File Name | Description | -|--------------------------|---------------------------------------------------------------| -| `gazan-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | -| `gazan-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | -| `gazan-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | -| `gazan-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | +| File Name | Description | +|---------------------------|---------------------------------------------------------------| +| `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency | +| `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies | +| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency | +| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies | ## ๐Ÿ”Œ Running the Proxy ```bash -./gazan -c path/to/main.yaml +./aralez -c path/to/main.yaml ``` ## ๐Ÿ”Œ Systemd integration ```bash -cat > /etc/systemd/system/gazan.service < /etc/systemd/system/aralez.service <` header. - To obtain JWT a token, you should send **generate** request to built in api server's `/jwt` endpoint. - `master_key`: should match configured `masterkey` in `main.yaml` and `upstreams.yaml`. @@ -255,7 +254,7 @@ curl -H "Authorization: Bearer ${TOK}" -H 'Host: myip.mydomain.com' http://127.0 With URL parameter (Very useful if you want to generate and share temporary links) ```bash -curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?gazantoken=${TOK}`" +curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?araleztoken=${TOK}`" ``` **Example Request with API Key** @@ -287,27 +286,27 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/ - Sticky session support. - HTTP2 ready. -๐Ÿ“Š Why Choose Gazan? โ€“ Feature Comparison +๐Ÿ“Š Why Choose Aralez? โ€“ Feature Comparison -| Feature | **Gazan** | **Nginx** | **HAProxy** | **Traefik** | -|----------------------------|---------------------------------------------------------------------|--------------------------|-------------------------|-----------------| -| **Hot Reload** | โœ… Yes (live, API/file) | โš ๏ธ Reloads config | โš ๏ธ Reloads config | โœ… Yes (dynamic) | -| **JWT Auth** | โœ… Built-in | โŒ External scripts | โŒ External Lua or agent | โš ๏ธ With plugins | -| **WebSocket Support** | โœ… Automatic | โš ๏ธ Manual config | โœ… Yes | โœ… Yes | -| **gRPC Support** | โœ… Automatic (no config) | โš ๏ธ Manual + HTTP/2 + TLS | โš ๏ธ Complex setup | โœ… Native | -| **TLS Termination** | โœ… Built-in (OpenSSL) | โœ… Yes | โœ… Yes | โœ… Yes | -| **TLS Upstream Detection** | โœ… Automatic | โŒ | โŒ | โŒ | -| **HTTP/2 Support** | โœ… Automatic | โš ๏ธ Requires extra config | โš ๏ธ Requires build flags | โœ… Native | -| **Sticky Sessions** | โœ… Cookie-based | โš ๏ธ In plus version only | โœ… | โœ… | -| **Prometheus Metrics** | โœ… [Built in](https://github.com/sadoyan/gazan/blob/main/METRICS.md) | โš ๏ธ With Lua or exporter | โš ๏ธ With external script | โœ… Native | -| **Built With** | ๐Ÿฆ€ Rust | C | C | Go | +| Feature | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | +|----------------------------|----------------------------------------------------------------------|--------------------------|-------------------------|-----------------| +| **Hot Reload** | โœ… Yes (live, API/file) | โš ๏ธ Reloads config | โš ๏ธ Reloads config | โœ… Yes (dynamic) | +| **JWT Auth** | โœ… Built-in | โŒ External scripts | โŒ External Lua or agent | โš ๏ธ With plugins | +| **WebSocket Support** | โœ… Automatic | โš ๏ธ Manual config | โœ… Yes | โœ… Yes | +| **gRPC Support** | โœ… Automatic (no config) | โš ๏ธ Manual + HTTP/2 + TLS | โš ๏ธ Complex setup | โœ… Native | +| **TLS Termination** | โœ… Built-in (OpenSSL) | โœ… Yes | โœ… Yes | โœ… Yes | +| **TLS Upstream Detection** | โœ… Automatic | โŒ | โŒ | โŒ | +| **HTTP/2 Support** | โœ… Automatic | โš ๏ธ Requires extra config | โš ๏ธ Requires build flags | โœ… Native | +| **Sticky Sessions** | โœ… Cookie-based | โš ๏ธ In plus version only | โœ… | โœ… | +| **Prometheus Metrics** | โœ… [Built in](https://github.com/sadoyan/aralez/blob/main/METRICS.md) | โš ๏ธ With Lua or exporter | โš ๏ธ With external script | โœ… Native | +| **Built With** | ๐Ÿฆ€ Rust | C | C | Go | ## ๐Ÿ’ก Simple benchmark by [Oha](https://github.com/hatoo/oha) โš ๏ธ These benchmarks use : - 3 async Rust echo servers on a local network with 1Gbit as upstreams. -- A dedicated server for running **Gazan** +- A dedicated server for running **Aralez** - A dedicated server for running **Oha** - The following upstreams configuration. - 9 test URLs from simple `/` to nested up to 7 subpaths. @@ -318,7 +317,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/ "/": to_https: false headers: - - "X-Proxy-From:Gazan" + - "X-Proxy-From:Aralez" servers: - "192.168.211.211:8000" - "192.168.211.212:8000" @@ -327,7 +326,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/ to_https: false headers: - "X-Some-Thing:Yaaaaaaaaaaaaaaa" - - "X-Proxy-From:Gazan" + - "X-Proxy-From:Aralez" servers: - "192.168.211.211:8000" - "192.168.211.212:8000" @@ -338,7 +337,7 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/ - CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - 300 : simultaneous connections - Duration : 10 Minutes -- Binary : gazan-x86_64-glibc +- Binary : aralez-x86_64-glibc ``` Summary: @@ -389,12 +388,12 @@ Error distribution: [158] aborted due to deadline ``` -![Gazan](https://netangels.net/utils/glibc10.png) +![Aralez](https://netangels.net/utils/glibc10.png) - CPU : Intel(R) Xeon(R) CPU E3-1270 v6 @ 3.80GHz - 300 : simultaneous connections - Duration : 10 Minutes -- Binary : gazan-x86_64-musl +- Binary : aralez-x86_64-musl ``` Summary: @@ -445,4 +444,4 @@ Error distribution: [228] aborted due to deadline ``` -![Gazan](https://netangels.net/utils/musl10.png) \ No newline at end of file +![Aralez](https://netangels.net/utils/musl10.png) \ No newline at end of file diff --git a/etc/main.yaml b/etc/main.yaml index 5ea48e3..526c059 100644 --- a/etc/main.yaml +++ b/etc/main.yaml @@ -1,20 +1,19 @@ # Main configuration file , applied on startup threads: 12 # Nubber of daemon threads default setting -#user: pastor # Username for running gazan after dropping root privileges, requires program to start as root -#group: pastor # Group for running gazan after dropping root privileges, requires program to start as root +#user: pastor # Username for running aralez after dropping root privileges, requires program to start as root +#group: pastor # Group for running aralez after dropping root privileges, requires program to start as root daemon: false # Run in background upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections -pid_file: /tmp/gazan.pid # Path to PID file -error_log: /tmp/gazan_err.log # Path to error log -upgrade_sock: /tmp/gazan.sock # Path to socket file +pid_file: /tmp/aralez.pid # Path to PID file +error_log: /tmp/aralez_err.log # Path to error log +upgrade_sock: /tmp/aralez.sock # Path to socket file config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location config_tls_address: 0.0.0.0:3001 # HTTP TLS API address for pushing upstreams.yaml from remote location config_tls_certificate: etc/server.crt # Mandatory if config_tls_address is set config_tls_key_file: etc/key.pem # Mandatory if config_tls_address is set 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 -tls_certificate: etc/server.crt # Mandatory if proxy_address_tls is set -tls_key_file: etc/key.pem # Mandatory if proxy_address_tls is set +proxy_certificates: etc/yoyo # Mandatory if proxy_address_tls set, should contain certificate and key files strictly in a format {NAME}.crt, {NAME}.key. upstreams_conf: etc/upstreams.yaml # the location of upstreams file log_level: info # info, warn, error, debug, trace, off hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE diff --git a/etc/stresstest.yaml b/etc/stresstest.yaml index 798d746..f24b67c 100644 --- a/etc/stresstest.yaml +++ b/etc/stresstest.yaml @@ -11,7 +11,7 @@ upstreams: "/": ssl: false headers: - - "X-Proxy-From:Gazan" + - "X-Proxy-From:Aralez" servers: - "192.168.221.213:8000" - "192.168.221.214:8000" diff --git a/src/utils.rs b/src/utils.rs index 2562713..4b1bf6c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,4 +7,5 @@ pub mod jwt; pub mod metrics; pub mod parceyaml; pub mod structs; +pub mod tls; pub mod tools; diff --git a/src/utils/auth.rs b/src/utils/auth.rs index d807e4f..d6cc600 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -37,7 +37,7 @@ impl AuthValidator for ApiKeyAuth<'_> { impl AuthValidator for JwtAuth<'_> { fn validate(&self, session: &Session) -> bool { let jwtsecret = self.0; - if let Some(tok) = get_query_param(session, "gazantoken") { + if let Some(tok) = get_query_param(session, "araleztoken") { return check_jwt(tok.as_str(), jwtsecret); } diff --git a/src/utils/healthcheck.rs b/src/utils/healthcheck.rs index 6c845b8..cc861a5 100644 --- a/src/utils/healthcheck.rs +++ b/src/utils/healthcheck.rs @@ -75,7 +75,7 @@ pub async fn hc2(upslist: Arc, fullist: Arc, if first_run == 1 { info!("Performing initial hatchecks and upstreams ssl detection"); clone_idmap_into(&totest, &idlist); - info!("Gazan is up and ready to serve requests, the upstreams list is:"); + info!("Aralez is up and ready to serve requests, the upstreams list is:"); print_upstreams(&totest) } diff --git a/src/utils/metrics.rs b/src/utils/metrics.rs index b39a0c9..bebe215 100644 --- a/src/utils/metrics.rs +++ b/src/utils/metrics.rs @@ -10,36 +10,36 @@ pub struct MetricTypes { } lazy_static::lazy_static! { pub static ref REQUEST_COUNT: IntCounter = register_int_counter!( - "gazan_requests_total", - "Total number of requests handled by Gazan" + "aralez_requests_total", + "Total number of requests handled by Aralez" ).unwrap(); pub static ref RESPONSE_CODES: IntCounterVec = register_int_counter_vec!( - "gazan_responses_total", + "aralez_responses_total", "Responses grouped by status code", &["status"] ).unwrap(); pub static ref REQUEST_LATENCY: Histogram = register_histogram!( - "gazan_request_latency_seconds", + "aralez_request_latency_seconds", "Request latency in seconds", vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0] ).unwrap(); pub static ref RESPONSE_LATENCY: Histogram = register_histogram!( - "gazan_response_latency_seconds", + "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 ref REQUESTS_BY_METHOD: IntCounterVec = register_int_counter_vec!( - "gazan_requests_by_method_total", + "aralez_requests_by_method_total", "Number of requests by HTTP method", &["method"] ).unwrap(); pub static ref REQUESTS_BY_VERSION: IntCounterVec = register_int_counter_vec!( - "gazan_requests_by_version_total", + "aralez_requests_by_version_total", "Number of requests by HTTP versions", &["version"] ).unwrap(); pub static ref ERROR_COUNT: IntCounter = register_int_counter!( - "gazan_errors_total", + "aralez_errors_total", "Total number of errors" ).unwrap(); } diff --git a/src/utils/structs.rs b/src/utils/structs.rs index c46bf6e..82f5408 100644 --- a/src/utils/structs.rs +++ b/src/utils/structs.rs @@ -73,7 +73,8 @@ pub struct AppConfig { pub config_tls_key_file: Option, pub proxy_address_tls: Option, pub proxy_port_tls: Option, - pub tls_certificate: Option, - pub tls_key_file: Option, + // pub tls_certificate: Option, + // pub tls_key_file: Option, pub local_server: Option<(String, u16)>, + pub proxy_certificates: Option, } diff --git a/src/utils/tls.rs b/src/utils/tls.rs new file mode 100644 index 0000000..5a43193 --- /dev/null +++ b/src/utils/tls.rs @@ -0,0 +1,176 @@ +use openssl::ssl::{select_next_proto, AlpnError, NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef}; +use rustls_pemfile::{read_one, Item}; +use serde::Deserialize; +use std::collections::HashSet; +use std::fs::File; +use std::io::BufReader; +use x509_parser::extensions::GeneralName; +use x509_parser::nom::Err as NomErr; +use x509_parser::prelude::*; + +#[derive(Clone, Deserialize, Debug)] +pub struct CertificateConfig { + pub cert_path: String, + pub key_path: String, +} + +#[derive(Debug)] +struct CertificateInfo { + common_names: Vec, + alt_names: Vec, + ssl_context: SslContext, + #[allow(dead_code)] + cert_path: String, // Only used for logging + #[allow(dead_code)] + key_path: String, // Only used for logging +} + +#[derive(Debug)] +pub struct Certificates { + configs: Vec, + pub default_cert_path: String, + pub default_key_path: String, +} + +impl Certificates { + pub fn new(configs: &Vec) -> Self { + let default_cert = configs.first().expect("atleast one TLS certificate required"); + let mut cert_infos = Vec::new(); + for config in configs { + cert_infos.push( + load_cert_info(&config.cert_path, &config.key_path) + .unwrap_or_else(|| panic!("unable to load certificate info | public: {}, private: {}", &config.cert_path, &config.key_path)), + ); + } + Self { + configs: cert_infos, + default_cert_path: default_cert.cert_path.clone(), + default_key_path: default_cert.key_path.clone(), + } + } + + fn find_ssl_context(&self, server_name: &str) -> Option<&SslContext> { + for config in &self.configs { + // Exact name match + if config.common_names.contains(&server_name.to_string()) || config.alt_names.contains(&server_name.to_string()) { + return Some(&config.ssl_context); + } + + // Wildcard match + for name in &config.common_names { + if name.starts_with("*.") && server_name.ends_with(&name[1..]) { + return Some(&config.ssl_context); + } + } + for name in &config.alt_names { + if name.starts_with("*.") && server_name.ends_with(&name[1..]) { + return Some(&config.ssl_context); + } + } + } + None + } + + pub fn server_name_callback(&self, ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert) -> Result<(), SniError> { + let server_name = ssl_ref.servername(NameType::HOST_NAME); + log::debug!("TLS connect: server_name = {:?}, ssl_ref = {:?}, ssl_alert = {:?}", server_name, ssl_ref, ssl_alert); + if let Some(name) = server_name { + match self.find_ssl_context(name) { + Some(ctx) => { + ssl_ref.set_ssl_context(ctx).map_err(|_| SniError::ALERT_FATAL)?; + } + None => { + log::debug!("No matching server name found"); + } + } + } + Ok(()) + } +} + +fn load_cert_info(cert_path: &str, key_path: &str) -> Option { + let mut common_names = HashSet::new(); + let mut alt_names = HashSet::new(); + + let file = File::open(cert_path); + match file { + Err(e) => { + log::error!("Failed to open certificate file: {:?}", e); + return None; + } + Ok(file) => { + let mut reader = BufReader::new(file); + match read_one(&mut reader) { + Err(e) => { + log::error!("Failed to decode PEM from certificate file: {:?}", e); + return None; + } + Ok(leaf) => match leaf { + Some(Item::X509Certificate(cert)) => match X509Certificate::from_der(&cert) { + Err(NomErr::Error(e)) | Err(NomErr::Failure(e)) => { + log::error!("Failed to parse certificate: {:?}", e); + return None; + } + Err(_) => { + log::error!("Unknown error while parsing certificate"); + return None; + } + Ok((_, x509)) => { + let subject = x509.subject(); + for attr in subject.iter_common_name() { + if let Ok(cn) = attr.as_str() { + common_names.insert(cn.to_string()); + } + } + + if let Ok(Some(san)) = x509.subject_alternative_name() { + for name in san.value.general_names.iter() { + if let GeneralName::DNSName(dns) = name { + let dns_string = dns.to_string(); + if !common_names.contains(&dns_string) { + alt_names.insert(dns_string); + } + } + } + } + } + }, + _ => { + log::error!("Failed to read certificate"); + return None; + } + }, + } + } + } + + if let Ok(ssl_context) = create_ssl_context(cert_path, key_path) { + Some(CertificateInfo { + cert_path: cert_path.to_string(), + key_path: key_path.to_string(), + common_names: common_names.into_iter().collect(), + alt_names: alt_names.into_iter().collect(), + ssl_context, + }) + } else { + log::error!("Failed to create SSL context from cert paths"); + None + } +} + +fn create_ssl_context(cert_path: &str, key_path: &str) -> Result> { + let mut ctx = SslContext::builder(SslMethod::tls())?; + + ctx.set_certificate_chain_file(cert_path)?; + ctx.set_private_key_file(key_path, SslFiletype::PEM)?; + ctx.set_alpn_select_callback(prefer_h2); + let built = ctx.build(); + Ok(built) +} + +pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> { + match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) { + Some(p) => Ok(p), + _ => Err(AlpnError::NOACK), + } +} diff --git a/src/utils/tools.rs b/src/utils/tools.rs index 87bf3f7..d7fe852 100644 --- a/src/utils/tools.rs +++ b/src/utils/tools.rs @@ -1,9 +1,11 @@ use crate::utils::structs::{UpstreamsDashMap, UpstreamsIdMap}; +use crate::utils::tls; use dashmap::DashMap; use sha2::{Digest, Sha256}; use std::any::type_name; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Write; +use std::fs; use std::sync::atomic::AtomicUsize; #[allow(dead_code)] @@ -146,3 +148,33 @@ pub fn clone_idmap_into(original: &UpstreamsDashMap, cloned: &UpstreamsIdMap) { } } } + +pub fn listdir(dir: String) -> Vec { + let mut f = HashMap::new(); + let mut certificate_configs: Vec = vec![]; + let paths = fs::read_dir(dir).unwrap(); + for path in paths { + let path_str = path.unwrap().path().to_str().unwrap().to_owned(); + if path_str.ends_with(".crt") { + let name = path_str.replace(".crt", ""); + let mut inner = vec![]; + let domain = name.split("/").collect::>(); + inner.push(name.clone() + ".crt"); + inner.push(name.clone() + ".key"); + f.insert(domain[domain.len() - 1].to_owned(), inner); + let y = tls::CertificateConfig { + cert_path: name.clone() + ".crt", + key_path: name.clone() + ".key", + }; + certificate_configs.push(y); + } + } + for (_, v) in f.iter() { + let y = tls::CertificateConfig { + cert_path: v[0].clone(), + key_path: v[1].clone(), + }; + certificate_configs.push(y); + } + certificate_configs +} diff --git a/src/web/proxyhttp.rs b/src/web/proxyhttp.rs index 84395d0..05c0801 100644 --- a/src/web/proxyhttp.rs +++ b/src/web/proxyhttp.rs @@ -4,9 +4,11 @@ use crate::utils::structs::{AppConfig, Extraparams, Headers, UpstreamsDashMap, U use crate::web::gethosts::GetHost; use arc_swap::ArcSwap; use async_trait::async_trait; +use axum::body::Bytes; use log::{debug, warn}; use pingora::http::{RequestHeader, ResponseHeader, StatusCode}; use pingora::prelude::*; +use pingora::ErrorSource::Upstream; use pingora_core::listeners::ALPN; use pingora_core::prelude::HttpPeer; use pingora_proxy::{ProxyHttp, Session}; @@ -108,14 +110,26 @@ impl ProxyHttp for LB { Ok(peer) } None => { - warn!("Upstream not found. Host: {:?}, Path: {}", hostname, session.req_header().uri); - Ok(return_no_host(&self.config.local_server)) + session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await.expect("Failed to send error"); + Err(Box::new(Error { + etype: HTTPStatus(502), + esource: Upstream, + retry: RetryType::Decided(false), + cause: None, + context: Option::from(ImmutStr::Static("Upstream not found")), + })) } } } None => { - warn!("Upstream not found. Host: {:?}, Path: {}", host_name, session.req_header().uri); - Ok(return_no_host(&self.config.local_server)) + session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await.expect("Failed to send error"); + Err(Box::new(Error { + etype: HTTPStatus(502), + esource: Upstream, + retry: RetryType::Decided(false), + cause: None, + context: None, + })) } } } @@ -213,9 +227,9 @@ fn return_header_host(session: &Session) -> Option<&str> { } } -fn return_no_host(inp: &Option<(String, u16)>) -> Box { - match inp { - Some(t) => Box::new(HttpPeer::new(t, false, String::new())), - None => Box::new(HttpPeer::new(("0.0.0.0", 0), false, String::new())), - } -} +// fn return_no_host(inp: &Option<(String, u16)>) -> Box { +// match inp { +// Some(t) => Box::new(HttpPeer::new(t, false, String::new())), +// None => Box::new(HttpPeer::new(("0.0.0.0", 0), false, String::new())), +// } +// } diff --git a/src/web/start.rs b/src/web/start.rs index 0e0eea6..2b62639 100644 --- a/src/web/start.rs +++ b/src/web/start.rs @@ -1,9 +1,13 @@ // use rustls::crypto::ring::default_provider; use crate::utils::structs::Extraparams; +use crate::utils::tls; +use crate::utils::tools::listdir; use crate::web::proxyhttp::LB; use arc_swap::ArcSwap; use dashmap::DashMap; use log::info; +use openssl::ssl::{SslAlert, SslRef}; +use pingora_core::listeners::tls::TlsSettings; use pingora_core::prelude::{background_service, Opt}; use pingora_core::server::Server; use std::env; @@ -77,13 +81,22 @@ pub fn run() { let bind_address_http = cfg.proxy_address_http.clone(); let bind_address_tls = cfg.proxy_address_tls.clone(); + // let foo = crate::utils::tls::build_ssl_context_builder(); match bind_address_tls { Some(bind_address_tls) => { info!("Running TLS listener on :{}", bind_address_tls); - let cert_path = cfg.tls_certificate.clone().unwrap(); - let key_path = cfg.tls_key_file.clone().unwrap(); - let mut tls_settings = pingora_core::listeners::tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + // let cert_path = cfg.tls_certificate.clone().unwrap(); + // let key_path = cfg.tls_key_file.clone().unwrap(); + // let mut tls_settings = tls::TlsSettings::intermediate(&cert_path, &key_path).unwrap(); + // tls_settings.enable_h2(); + // proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings); + + let certificate_configs = listdir(cfg.proxy_certificates.clone().unwrap()); + let certificates = tls::Certificates::new(&certificate_configs); + let mut tls_settings = TlsSettings::intermediate(&certificates.default_cert_path, &certificates.default_key_path).expect("unable to load or parse cert/key"); tls_settings.enable_h2(); + tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certificates.server_name_callback(ssl_ref, ssl_alert)); + tls_settings.set_alpn_select_callback(tls::prefer_h2); proxy.add_tls_with_settings(&bind_address_tls, None, tls_settings); } None => {} diff --git a/src/web/webserver.rs b/src/web/webserver.rs index 8f8c794..dd015f2 100644 --- a/src/web/webserver.rs +++ b/src/web/webserver.rs @@ -4,7 +4,7 @@ use axum::body::Body; use axum::extract::{Query, State}; use axum::http::{Response, StatusCode}; use axum::response::IntoResponse; -use axum::routing::{delete, get, head, post, put}; +use axum::routing::{get, post}; use axum::{Json, Router}; use axum_server::tls_openssl::OpenSSLConfig; use futures::channel::mpsc::Sender; @@ -43,11 +43,11 @@ pub async fn run_server(config: &APIUpstreamProvider, mut to_return: Sender impl IntoResponse { .unwrap() } -#[allow(dead_code)] -async fn senderror() -> impl IntoResponse { - Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() -} +// #[allow(dead_code)] +// async fn senderror() -> impl IntoResponse { +// Response::builder().status(StatusCode::BAD_GATEWAY).body(Body::from("No live upstream found!\n")).unwrap() +// }